blob: f7047c9fb23c9dce1311fb26e154252268e27d79 [file] [log] [blame]
// Copyright 2019 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#ifndef IREE_VM_STACK_H_
#define IREE_VM_STACK_H_
#include <stddef.h>
#include <stdint.h>
#include "iree/base/api.h"
#include "iree/vm/module.h"
#include "iree/vm/ref.h"
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
// A reasonable default stack storage size, in bytes.
// This will allow most (reasonable) programs to run. If running
// unverified/untested programs then prefer to use a dynamically growable stack
// until the expectations of the programs are checked; for example, hopefully
// in a year or two we have much more complex models with much deeper call
// stacks and we may want to re-evaluate the host-stack allocation size.
//
// The value was chosen to fit quite a few i32 registers and a reasonable amount
// of ref registers (that are 2 * sizeof(void*)). For many invocations this will
// be more than enough to perform the work without needing an additional dynamic
// allocation/resize.
#define IREE_VM_STACK_DEFAULT_SIZE (8 * 1024)
// The minimum size of VM stack storage.
#define IREE_VM_STACK_MIN_SIZE (1 * 1024)
// The maximum size of VM stack storage; anything larger is probably a bug.
#define IREE_VM_STACK_MAX_SIZE (1 * 1024 * 1024)
enum iree_vm_invocation_flag_bits_t {
IREE_VM_INVOCATION_FLAG_NONE = 0u,
// Enables tracing of execution to stderr (when available) for the invocation.
// See iree/base/config.h for the flags that control whether this
// functionality is available; specifically:
// -DIREE_VM_EXECUTION_TRACING_ENABLE=1
IREE_VM_INVOCATION_FLAG_TRACE_EXECUTION = 1u << 0,
// Attributes invocation timings to the caller instead of a context or
// invocation-specific fiber.
IREE_VM_INVOCATION_FLAG_TRACE_INLINE = 1u << 1,
};
typedef uint32_t iree_vm_invocation_flags_t;
typedef enum iree_vm_stack_frame_type_e {
// Represents an `[external]` frame that needs to marshal args/results.
// These frames have no source location and are tracked so that we know when
// transitions occur into/out-of external code.
IREE_VM_STACK_FRAME_EXTERNAL = 0,
// Represents a `[native]` frame that has no persistent register storage.
// These frames may have source location information provided by the
// implementation.
IREE_VM_STACK_FRAME_NATIVE = 1,
// VM stack frame in bytecode using internal register storage.
IREE_VM_STACK_FRAME_BYTECODE = 2,
// Wait frame used to retain resources during a yield-and-wait.
// Execution cannot continue until the wait conditions are met.
IREE_VM_STACK_FRAME_WAIT = 3,
} iree_vm_stack_frame_type_t;
// A single stack frame within the VM.
//
// NOTE: to (try to) get better cache hit rates we put the most frequently
// accessed members **LAST**. This is because the custom frame storage data
// immediately follows this struct in memory and is highly likely to be touched
// by the callee immediately and repeatedly.
typedef struct iree_vm_stack_frame_t {
// Stack frame type used to determine which fields are valid.
iree_vm_stack_frame_type_t type;
// Function that the stack frame is within.
iree_vm_function_t function;
// Cached module state pointer for the module containing |function|.
// This removes the need to lookup the module state when control returns to
// the function during continuation or from a return instruction.
iree_vm_module_state_t* module_state;
// Current program counter within the function.
// Implementations may treat this offset differently, treating it as a byte
// offset (such as in the case of VM bytecode), a block identifier (compiled
// code), etc.
iree_vm_source_offset_t pc;
// Depth of the frame within the stack.
// As stack frame pointers are not stable this can be used instead to detect
// stack enter/leave balance issues.
int32_t depth;
} iree_vm_stack_frame_t;
// Defines the type of wait operation in a IREE_VM_STACK_FRAME_WAIT frame.
enum iree_vm_wait_type_e {
// Sleeps until the timeout is reached then resumes execution.
// Immediate timeouts may be used to perform a deferred call.
IREE_VM_WAIT_UNTIL,
// Waits until one or more wait sources have resolved then resumes execution.
IREE_VM_WAIT_ANY,
// Waits until all of the wait sources have resolved then resumes execution.
IREE_VM_WAIT_ALL,
};
typedef uint8_t iree_vm_wait_type_t;
// Stack frame storage for wait frames.
// These keep track of the wait parameters on the top of the stack until the
// wait condition has been satisfied.
typedef struct iree_vm_wait_frame_t {
// Type of wait being performed.
iree_vm_wait_type_t wait_type;
// Status of the wait operation set by the scheduler.
iree_status_t wait_status;
// Maximum time to wait before failing the wait with
// IREE_STATUS_DEADLINE_EXCEEDED.
iree_time_t deadline_ns;
// Optional tracing zone that marks the beginning of the wait operation.
// Waiters are expected to end this zone after leaving the wait frame.
IREE_TRACE(iree_zone_id_t trace_zone;)
// Total number of wait sources.
iree_host_size_t count;
// List of wait source to wait on.
iree_wait_source_t wait_sources[];
} iree_vm_wait_frame_t;
// Result of a wait operation.
typedef struct iree_vm_wait_result_t {
// Final status of the wait operation; OK if successful and DEADLINE_EXCEEDED
// if the deadline was reached before all conditions were met. Other failures
// may occur if for example the context is aborted.
iree_status_t status;
// Optional tracing zone provided by the waiter when entering the frame.
// The caller performing the wait frame leave must end it.
IREE_TRACE(iree_zone_id_t trace_zone;)
} iree_vm_wait_result_t;
// Returns the implementation-defined frame storage associated with |frame|.
// The pointer will contain at least as many bytes as requested by frame_size.
static inline void* iree_vm_stack_frame_storage(iree_vm_stack_frame_t* frame) {
IREE_ASSERT(frame);
IREE_ASSERT_ALIGNED(frame, sizeof(iree_max_align_t));
return (void*)((uintptr_t)frame + sizeof(iree_vm_stack_frame_t));
}
// Callback for cleaning up stack frame storage before a frame is left or the
// stack is destroyed.
typedef void(IREE_API_PTR* iree_vm_stack_frame_cleanup_fn_t)(
iree_vm_stack_frame_t* frame);
// A state resolver that can allocate or lookup module state.
typedef struct iree_vm_state_resolver_t {
void* self;
iree_status_t(IREE_API_PTR* query_module_state)(
void* state_resolver, iree_vm_module_t* module,
iree_vm_module_state_t** out_module_state);
} iree_vm_state_resolver_t;
// A fiber stack used for storing stack frame state during execution.
// All required state is stored within the stack and no host thread-local state
// is used allowing us to execute multiple fibers on the same host thread.
typedef struct iree_vm_stack_t iree_vm_stack_t;
// Defines and initializes an inline VM stack.
// The stack will be ready for use and must be deinitialized with
// iree_vm_stack_deinitialize when no longer required.
//
// Example:
// IREE_VM_INLINE_STACK_INITIALIZE(
// stack,
// IREE_VM_INVOCATION_FLAG_NONE,
// iree_vm_context_state_resolver(context),
// iree_allocator_system());
// ...
// iree_vm_stack_deinitialize(stack);
#define IREE_VM_INLINE_STACK_INITIALIZE(stack, flags, state_resolver, \
allocator) \
uint8_t __stack_storage[IREE_VM_STACK_DEFAULT_SIZE]; \
iree_byte_span_t __stack_storage_span = \
iree_make_byte_span(__stack_storage, sizeof(__stack_storage)); \
iree_vm_stack_t* stack = NULL; \
IREE_IGNORE_ERROR(iree_vm_stack_initialize( \
__stack_storage_span, (flags), (state_resolver), (allocator), &stack));
// Initializes a statically-allocated stack in |storage|.
// The contents of the |storage| can be anything upon initialization and the
// stack must be deinitialized with iree_vm_stack_deinitialize before the
// storage is freed. The provided |allocator| is only used for stack growth
// beyond the initial storage capacity and may be iree_allocator_null() to
// prevent growth. Use IREE_VM_STACK_DEFAULT_SIZE for a reasonable default or
// use iree_vm_stack_allocate if the input programs may exceed reason.
//
// The provided |state_resolver| will be used to resolve a module to a module
// state within a context. This will be called on function entry whenever module
// transitions occur.
//
// Example:
// uint8_t stack_storage[IREE_VM_STACK_DEFAULT_SIZE];
// iree_vm_stack_t* stack = NULL;
// iree_vm_stack_initialize(stack_storage, ..., &stack);
// ...
// iree_vm_stack_deinitialize(stack);
// // stack_storage can now be reused/freed/etc
IREE_API_EXPORT iree_status_t iree_vm_stack_initialize(
iree_byte_span_t storage, iree_vm_invocation_flags_t flags,
iree_vm_state_resolver_t state_resolver, iree_allocator_t allocator,
iree_vm_stack_t** out_stack);
// Resets the stack to its initial state by popping all stack frames.
IREE_API_EXPORT void iree_vm_stack_reset(iree_vm_stack_t* stack);
// Deinitializes a statically-allocated |stack| previously initialized with
// iree_vm_stack_initialize.
IREE_API_EXPORT void iree_vm_stack_deinitialize(iree_vm_stack_t* stack);
// Allocates a dynamically-growable stack.
//
// The provided |state_resolver| will be used to resolve a module to a module
// state within a context. This will be called on function entry whenever module
// transitions occur.
//
// The stack will be allocated from |allocator| and returned in |out_stack|.
// It must be freed with iree_vm_stack_free.
//
// Example:
// iree_vm_stack_t* stack = NULL;
// iree_vm_stack_allocate(..., iree_allocator_system(), &stack);
// ...
// iree_vm_stack_free(stack);
IREE_API_EXPORT iree_status_t iree_vm_stack_allocate(
iree_vm_invocation_flags_t flags, iree_vm_state_resolver_t state_resolver,
iree_allocator_t allocator, iree_vm_stack_t** out_stack);
// Frees a dynamically-allocated |stack| from iree_vm_stack_allocate.
IREE_API_EXPORT void iree_vm_stack_free(iree_vm_stack_t* stack);
// Returns the allocator used for growing the stack.
IREE_API_EXPORT iree_allocator_t
iree_vm_stack_allocator(const iree_vm_stack_t* stack);
// Returns the flags controlling the invocation this stack is used with.
IREE_API_EXPORT iree_vm_invocation_flags_t
iree_vm_stack_invocation_flags(const iree_vm_stack_t* stack);
// Returns the top stack execution frame, ignore wait frames.
IREE_API_EXPORT iree_vm_stack_frame_t* iree_vm_stack_top(
iree_vm_stack_t* stack);
// Returns the current stack frame or nullptr if the stack is empty.
IREE_API_EXPORT iree_vm_stack_frame_t* iree_vm_stack_current_frame(
iree_vm_stack_t* stack);
// Returns the parent stack frame or nullptr if the stack is empty.
IREE_API_EXPORT iree_vm_stack_frame_t* iree_vm_stack_parent_frame(
iree_vm_stack_t* stack);
// Queries the context-specific module state for the given module.
IREE_API_EXPORT iree_status_t iree_vm_stack_query_module_state(
iree_vm_stack_t* stack, iree_vm_module_t* module,
iree_vm_module_state_t** out_module_state);
// Enters into the given |wait_type| operation with a capacity of |wait_count|.
// May invalidate any pointers to stack frames and the only pointer that can
// be assumed valid after return is the one in |out_wait_frame|.
//
// The resulting |out_wait_frame| storage must have all wait sources populated
// by the caller upon return. Callers should then return to the scheduling loop
// by propagating an IREE_STATUS_DEFERRED result to ancestors.
//
// If the caller has an open trace zone it can provide it here to then retrieve
// it from iree_vm_stack_wait_leave in order to leave it.
//
// After the wait has completed (successfully or otherwise) the scheduler must
// call iree_vm_stack_wait_leave to pop the wait from the stack and resume
// execution.
IREE_API_EXPORT iree_status_t iree_vm_stack_wait_enter(
iree_vm_stack_t* stack, iree_vm_wait_type_t wait_type,
iree_host_size_t wait_count, iree_timeout_t timeout,
iree_zone_id_t trace_zone, iree_vm_wait_frame_t** out_wait_frame);
// Leaves the current wait frame on the top of the stack.
// |out_wait_result| will be populated with the results of the wait, such as
// IREE_STATUS_DEADLINE_EXCEEDED or IREE_STATUS_ABORTED if it failed.
// If a trace zone was passed to the iree_vm_stack_wait_enter it will be
// available for the caller to leave.
IREE_API_EXPORT iree_status_t iree_vm_stack_wait_leave(
iree_vm_stack_t* stack, iree_vm_wait_result_t* out_wait_result);
// Enters into the given |function| and returns the callee stack frame.
// May invalidate any pointers to stack frames and the only pointer that can be
// assumed valid after return is the one in |out_callee_frame|.
//
// |frame_size| can optionally be used to allocate storage within the stack for
// callee data. |frame_cleanup_fn| will be called when the frame is left either
// normally via an iree_vm_stack_function_leave call or if an error occurs and
// the stack needs to be torn down.
IREE_API_EXPORT iree_status_t iree_vm_stack_function_enter(
iree_vm_stack_t* stack, const iree_vm_function_t* function,
iree_vm_stack_frame_type_t frame_type, iree_host_size_t frame_size,
iree_vm_stack_frame_cleanup_fn_t frame_cleanup_fn,
iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame);
// Leaves the current stack frame.
IREE_API_EXPORT iree_status_t
iree_vm_stack_function_leave(iree_vm_stack_t* stack);
// Formats a backtrace of the current stack to the given string |builder|.
IREE_API_EXPORT iree_status_t iree_vm_stack_format_backtrace(
iree_vm_stack_t* stack, iree_string_builder_t* builder);
// Annotates |status| with the backtrace of |stack| and returns |base_status|.
IREE_API_EXPORT IREE_MUST_USE_RESULT iree_status_t
iree_vm_stack_annotate_backtrace(iree_vm_stack_t* stack,
iree_status_t base_status);
#if IREE_VM_BACKTRACE_ENABLE && \
(IREE_STATUS_FEATURES & IREE_STATUS_FEATURE_ANNOTATIONS)
#define IREE_VM_STACK_ANNOTATE_BACKTRACE_IF_ENABLED(stack, base_status) \
iree_vm_stack_annotate_backtrace(stack, base_status)
#else
#define IREE_VM_STACK_ANNOTATE_BACKTRACE_IF_ENABLED(stack, base_status) \
(base_status)
#endif // IREE_VM_BACKTRACE_ENABLE && IREE_STATUS_FEATURE_ANNOTATIONS
// Suspends active trace zones for all stack frames.
// This returns the zone stack to a state before entering the stack. To restore
// the zone stack use iree_vm_stack_resume_trace_zones.
//
// Trace zone suspend/resume is only required if the tracing library doesn't
// natively support fibers. By doing this manual zone suspend/resume we can
// leave the zone stack empty and avoid out-of-order zone events.
//
// At a yield point:
// z0 = [invoke]
// fiber:
// z1 = [function]
// z2 = [wait]
// After suspend:
// z0 = [invoke]
// After resume:
// z0 = [invoke]
// fiber:
// z1 = [function]
// z2 = [wait]
IREE_API_EXPORT void iree_vm_stack_suspend_trace_zones(iree_vm_stack_t* stack);
// Resumes trace zones for all stack frames.
// This recovers the zone stack from a prior iree_vm_stack_suspend_trace_zones.
// This is a no-op if the tracing library natively supports fibers.
IREE_API_EXPORT void iree_vm_stack_resume_trace_zones(iree_vm_stack_t* stack);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif // IREE_VM_STACK_H_