design-b D1a: scheduler context primitives on emscripten fibers
Doc 20 §6 D1, first slice: wasm/sched/context.{h,cpp} implements
create / yield_park / mark_ready / drain with the registry as truth.
The shape that matters — a STAR, not libcontext's symmetric swap:
contexts only ever swap OUT to the scheduler, and only the scheduler
swaps IN. So "is this target safe to enter?" stops being a guess
(libcontext's swap_suspended / parked / hot-main refusals) and becomes a
lookup: the registry says Parked/Ready and holds the buffer. Doc 19's
refused-resume is unrepresentable here because resume is not a decision
made at the swap site.
Enforced, not hoped for:
- at most one transition in flight (drain() refuses re-entry, so a
context calling drain() cannot turn the star into a cycle);
- mark_ready() never resumes inline — it queues, and drain() resumes
from a clean stack (doc 13 §1.4's deferred-wake law, per context);
- yield_park() off a context is REFUSED, which is the "nothing parks in
place" rule made mechanical;
- destroy() on a non-Finished context is refused (freeing a parked
stack strands whatever is on it);
- FIFO ready queue (no starvation);
- main-thread-only, per doc 21 §2's pthread finding.
Memory is accounted from the start (doc 20 risk 1): live/peak contexts,
bytes/peak bytes, and per-context asyncify high-water measured from
asyncify_data.stack_ptr — the same quantity the shim reports as `rem=`,
from the other end. Sizes deliberately start at 128 KB C stack + 128 KB
asyncify buffer rather than inheriting libcontext's 512 K, so the number
gets derived from evidence; a >75% buffer use beacons BUFFER-PRESSURE
because that overflow is silent corruption, not a crash.
No production path runs on this yet — dispatch moves at D2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
This commit is contained in:
parent
832cbcdf91
commit
98731cd30e
2 changed files with 634 additions and 0 deletions
508
wasm/sched/context.cpp
Normal file
508
wasm/sched/context.cpp
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* Scheduler contexts — implementation. See context.h for the contract and why
|
||||
* this is a star topology rather than libcontext's symmetric swap.
|
||||
*/
|
||||
#include "context.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/fiber.h>
|
||||
#include <emscripten/threading.h>
|
||||
|
||||
namespace pcbjam_sched
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Sizes are DELIBERATELY not libcontext's 512K asyncify buffer: doc 20 risk 1
|
||||
// requires them re-derived from measurement, and this layer measures its own
|
||||
// high-water use (see asyncify_used()). Start at a quarter of libcontext's and
|
||||
// let the gate tell us whether that is generous or tight — an inherited
|
||||
// constant nobody can justify is exactly what the risk warns about.
|
||||
//
|
||||
// The C stack is separate from the asyncify buffer: the former holds live
|
||||
// frames while running, the latter holds their locals while parked.
|
||||
constexpr size_t DEFAULT_C_STACK_BYTES = 128 * 1024;
|
||||
constexpr size_t DEFAULT_ASYNCIFY_BYTES = 128 * 1024;
|
||||
|
||||
// A hard ceiling turns "contexts leaked until the tab died" into a loud,
|
||||
// early failure with a registry dump. Deliberately low for D1: nothing in
|
||||
// production runs on contexts yet, and the test app's worst battery uses ~8.
|
||||
constexpr size_t MAX_LIVE_CONTEXTS = 64;
|
||||
|
||||
struct Context
|
||||
{
|
||||
ContextId id = 0;
|
||||
const char* label = "";
|
||||
const char* park_reason = "";
|
||||
Status status = Status::Fresh;
|
||||
int result = 0;
|
||||
|
||||
emscripten_fiber_t fiber {};
|
||||
std::vector<char> c_stack;
|
||||
std::vector<char> asyncify_stack;
|
||||
|
||||
void ( *entry )( void* ) = nullptr;
|
||||
void* arg = nullptr;
|
||||
|
||||
uint32_t parks = 0;
|
||||
uint32_t resumes = 0;
|
||||
size_t asyncify_high_water = 0;
|
||||
};
|
||||
|
||||
struct Registry
|
||||
{
|
||||
std::map<ContextId, Context*> contexts;
|
||||
std::vector<ContextId> ready_fifo; // FIFO: no starvation (doc 13 §1.5 inv. 8)
|
||||
|
||||
ContextId next_id = 1;
|
||||
ContextId running = 0; // 0 = the scheduler stack is running
|
||||
bool transition = false; // at most one swap in flight
|
||||
bool scheduler_initialized = false;
|
||||
emscripten_fiber_t scheduler_fiber {};
|
||||
std::vector<char> scheduler_asyncify_stack;
|
||||
|
||||
// Counters (stats_json)
|
||||
uint32_t created = 0;
|
||||
uint32_t finished = 0;
|
||||
uint32_t transitions = 0;
|
||||
uint32_t refusals = 0;
|
||||
size_t live = 0;
|
||||
size_t peak_live = 0;
|
||||
size_t bytes = 0;
|
||||
size_t peak_bytes = 0;
|
||||
size_t asyncify_high_water = 0;
|
||||
};
|
||||
|
||||
Registry& reg()
|
||||
{
|
||||
static Registry s_registry;
|
||||
return s_registry;
|
||||
}
|
||||
|
||||
void beacon( const char* aWhat, const char* aDetail, unsigned aId )
|
||||
{
|
||||
std::printf( "[sched-ctx] %s id=%u %s\n", aWhat, aId, aDetail ? aDetail : "" );
|
||||
std::fflush( stdout );
|
||||
}
|
||||
|
||||
bool on_main_thread()
|
||||
{
|
||||
// Contexts are a main-thread concept by construction (doc 21 §2). A worker
|
||||
// has its own Asyncify state; a context swapped there would corrupt both.
|
||||
return emscripten_is_main_runtime_thread() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bytes of asyncify buffer this context has consumed at its deepest park.
|
||||
*
|
||||
* asyncify_data_t is {stack_ptr, stack_limit, rewind_id}; the unwind fills
|
||||
* from the buffer's base upward, so used = stack_ptr - base. This is the same
|
||||
* quantity the shim's `rem=` telemetry reports from the other direction
|
||||
* (limit - ptr), and it is what doc 20 risk 1 wants sizing derived from.
|
||||
*/
|
||||
size_t asyncify_used( const Context& aCtx )
|
||||
{
|
||||
const char* base = aCtx.asyncify_stack.data();
|
||||
const char* ptr = static_cast<const char*>( aCtx.fiber.asyncify_data.stack_ptr );
|
||||
|
||||
if( !base || !ptr || ptr < base )
|
||||
return 0;
|
||||
|
||||
const size_t used = static_cast<size_t>( ptr - base );
|
||||
return used > aCtx.asyncify_stack.size() ? aCtx.asyncify_stack.size() : used;
|
||||
}
|
||||
|
||||
void note_asyncify_use( Context& aCtx )
|
||||
{
|
||||
const size_t used = asyncify_used( aCtx );
|
||||
|
||||
if( used > aCtx.asyncify_high_water )
|
||||
aCtx.asyncify_high_water = used;
|
||||
|
||||
if( used > reg().asyncify_high_water )
|
||||
reg().asyncify_high_water = used;
|
||||
|
||||
// Overflow here is silent corruption (libcontext's 512K comment documents
|
||||
// exactly that failure), so shout well before the edge rather than after.
|
||||
if( used * 4 > aCtx.asyncify_stack.size() * 3 )
|
||||
{
|
||||
char detail[128];
|
||||
std::snprintf( detail, sizeof( detail ),
|
||||
"asyncify buffer >75%% used (%zu/%zu) - raise the size",
|
||||
used, aCtx.asyncify_stack.size() );
|
||||
beacon( "BUFFER-PRESSURE", detail, aCtx.id );
|
||||
}
|
||||
}
|
||||
|
||||
Context* find( ContextId aId )
|
||||
{
|
||||
auto it = reg().contexts.find( aId );
|
||||
return it == reg().contexts.end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
void ensure_scheduler_context()
|
||||
{
|
||||
Registry& r = reg();
|
||||
|
||||
if( r.scheduler_initialized )
|
||||
return;
|
||||
|
||||
// The scheduler runs on whatever stack called drain() first — a fresh JS
|
||||
// task entry. Contexts swap back INTO this fiber, which is what makes the
|
||||
// topology a star: every yield lands here, and only here decides who runs
|
||||
// next.
|
||||
r.scheduler_asyncify_stack.assign( DEFAULT_ASYNCIFY_BYTES, 0 );
|
||||
emscripten_fiber_init_from_current_context( &r.scheduler_fiber,
|
||||
r.scheduler_asyncify_stack.data(),
|
||||
r.scheduler_asyncify_stack.size() );
|
||||
r.scheduler_initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context trampoline. Per emscripten fiber.h the entry function must NEVER
|
||||
* return (returning ends the whole program), so it loops: run the body, mark
|
||||
* Finished, swap back to the scheduler, and if the scheduler ever enters this
|
||||
* context again, run again.
|
||||
*/
|
||||
void context_trampoline( void* aArg )
|
||||
{
|
||||
auto* ctx = static_cast<Context*>( aArg );
|
||||
|
||||
while( true )
|
||||
{
|
||||
ctx->status = Status::Running;
|
||||
|
||||
if( ctx->entry )
|
||||
ctx->entry( ctx->arg );
|
||||
|
||||
ctx->status = Status::Finished;
|
||||
ctx->park_reason = "finished";
|
||||
reg().finished++;
|
||||
reg().running = 0;
|
||||
reg().transition = false;
|
||||
|
||||
// Back to the scheduler. A finished context is never re-entered by
|
||||
// drain() (it only picks Ready), so this swap is terminal in practice.
|
||||
emscripten_fiber_swap( &ctx->fiber, ®().scheduler_fiber );
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
const char* status_name( Status aStatus )
|
||||
{
|
||||
switch( aStatus )
|
||||
{
|
||||
case Status::Fresh: return "fresh";
|
||||
case Status::Running: return "running";
|
||||
case Status::Parked: return "parked";
|
||||
case Status::Ready: return "ready";
|
||||
case Status::Finished: return "finished";
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
|
||||
|
||||
ContextId create( void ( *aEntry )( void* ), void* aArg, const char* aLabel )
|
||||
{
|
||||
Registry& r = reg();
|
||||
|
||||
if( !on_main_thread() )
|
||||
{
|
||||
beacon( "REFUSED", "create() off the main thread", 0 );
|
||||
r.refusals++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( !aEntry )
|
||||
{
|
||||
beacon( "REFUSED", "create() with a null entry", 0 );
|
||||
r.refusals++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( r.live >= MAX_LIVE_CONTEXTS )
|
||||
{
|
||||
char detail[160];
|
||||
std::snprintf( detail, sizeof( detail ),
|
||||
"context ceiling reached (%zu live) - registry: %s",
|
||||
r.live, registry_json().c_str() );
|
||||
beacon( "REFUSED", detail, 0 );
|
||||
r.refusals++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto* ctx = new( std::nothrow ) Context();
|
||||
|
||||
if( !ctx )
|
||||
{
|
||||
beacon( "REFUSED", "context allocation failed", 0 );
|
||||
r.refusals++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx->id = r.next_id++;
|
||||
ctx->label = aLabel ? aLabel : "";
|
||||
ctx->entry = aEntry;
|
||||
ctx->arg = aArg;
|
||||
ctx->c_stack.assign( DEFAULT_C_STACK_BYTES, 0 );
|
||||
ctx->asyncify_stack.assign( DEFAULT_ASYNCIFY_BYTES, 0 );
|
||||
|
||||
emscripten_fiber_init( &ctx->fiber, context_trampoline, ctx,
|
||||
ctx->c_stack.data(), ctx->c_stack.size(),
|
||||
ctx->asyncify_stack.data(), ctx->asyncify_stack.size() );
|
||||
|
||||
ctx->status = Status::Ready; // enters at aEntry on the first drain()
|
||||
ctx->park_reason = "created";
|
||||
|
||||
r.contexts[ctx->id] = ctx;
|
||||
r.ready_fifo.push_back( ctx->id );
|
||||
r.created++;
|
||||
r.live++;
|
||||
|
||||
if( r.live > r.peak_live )
|
||||
r.peak_live = r.live;
|
||||
|
||||
r.bytes += ctx->c_stack.size() + ctx->asyncify_stack.size();
|
||||
|
||||
if( r.bytes > r.peak_bytes )
|
||||
r.peak_bytes = r.bytes;
|
||||
|
||||
return ctx->id;
|
||||
}
|
||||
|
||||
|
||||
int yield_park( const char* aReason )
|
||||
{
|
||||
Registry& r = reg();
|
||||
Context* ctx = find( r.running );
|
||||
|
||||
if( !ctx )
|
||||
{
|
||||
// Called on the scheduler stack: there is nothing to yield. This is
|
||||
// the "parked in place" mistake the whole design forbids, so it is a
|
||||
// loud refusal rather than a silent no-op.
|
||||
beacon( "REFUSED", "yield_park() with no running context", 0 );
|
||||
r.refusals++;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx->status = Status::Parked;
|
||||
ctx->park_reason = aReason ? aReason : "";
|
||||
ctx->parks++;
|
||||
r.running = 0;
|
||||
r.transition = false; // the swap below completes this transition
|
||||
|
||||
// Yield to the scheduler. Control returns here when drain() swaps us back
|
||||
// in after mark_ready() — and ONLY then, because Parked→Ready→resume is
|
||||
// the single path in.
|
||||
emscripten_fiber_swap( &ctx->fiber, &r.scheduler_fiber );
|
||||
|
||||
// Resumed. The registry set status/result before swapping in.
|
||||
note_asyncify_use( *ctx );
|
||||
return ctx->result;
|
||||
}
|
||||
|
||||
|
||||
bool mark_ready( ContextId aId, int aResult )
|
||||
{
|
||||
Registry& r = reg();
|
||||
Context* ctx = find( aId );
|
||||
|
||||
if( !ctx )
|
||||
{
|
||||
beacon( "REFUSED", "mark_ready() for an unknown context", aId );
|
||||
r.refusals++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ctx->status != Status::Parked )
|
||||
{
|
||||
char detail[96];
|
||||
std::snprintf( detail, sizeof( detail ), "mark_ready() on a %s context",
|
||||
status_name( ctx->status ) );
|
||||
beacon( "REFUSED", detail, aId );
|
||||
r.refusals++;
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx->result = aResult;
|
||||
ctx->status = Status::Ready;
|
||||
r.ready_fifo.push_back( aId );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
ContextId drain()
|
||||
{
|
||||
Registry& r = reg();
|
||||
|
||||
if( !on_main_thread() )
|
||||
{
|
||||
beacon( "REFUSED", "drain() off the main thread", 0 );
|
||||
r.refusals++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One transition at a time, and never re-entrantly from a context (a
|
||||
// context reaching drain() would make the star a cycle).
|
||||
if( r.transition || r.running != 0 )
|
||||
return 0;
|
||||
|
||||
ensure_scheduler_context();
|
||||
|
||||
// Skip ids that died or were consumed while queued.
|
||||
ContextId id = 0;
|
||||
Context* ctx = nullptr;
|
||||
|
||||
while( !r.ready_fifo.empty() )
|
||||
{
|
||||
id = r.ready_fifo.front();
|
||||
r.ready_fifo.erase( r.ready_fifo.begin() );
|
||||
ctx = find( id );
|
||||
|
||||
if( ctx && ctx->status == Status::Ready )
|
||||
break;
|
||||
|
||||
ctx = nullptr;
|
||||
}
|
||||
|
||||
if( !ctx )
|
||||
return 0;
|
||||
|
||||
r.transition = true;
|
||||
r.transitions++;
|
||||
r.running = id;
|
||||
ctx->status = Status::Running;
|
||||
ctx->resumes++;
|
||||
|
||||
// Swap in. Returns when the context parks (yield_park) or finishes; both
|
||||
// clear running/transition before swapping back.
|
||||
emscripten_fiber_swap( &r.scheduler_fiber, &ctx->fiber );
|
||||
|
||||
r.transition = false;
|
||||
r.running = 0;
|
||||
|
||||
// The context object may still exist (parked) or be finished; either way
|
||||
// its buffer use is now measurable.
|
||||
if( Context* back = find( id ) )
|
||||
note_asyncify_use( *back );
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
bool transition_in_flight()
|
||||
{
|
||||
return reg().transition;
|
||||
}
|
||||
|
||||
|
||||
ContextId current()
|
||||
{
|
||||
return reg().running;
|
||||
}
|
||||
|
||||
|
||||
Status status_of( ContextId aId )
|
||||
{
|
||||
Context* ctx = find( aId );
|
||||
return ctx ? ctx->status : Status::Finished;
|
||||
}
|
||||
|
||||
|
||||
bool destroy( ContextId aId )
|
||||
{
|
||||
Registry& r = reg();
|
||||
Context* ctx = find( aId );
|
||||
|
||||
if( !ctx )
|
||||
return false;
|
||||
|
||||
if( ctx->status != Status::Finished )
|
||||
{
|
||||
// Freeing a parked context's stack would strand whatever is on it —
|
||||
// the very failure mode this layer exists to make impossible.
|
||||
char detail[96];
|
||||
std::snprintf( detail, sizeof( detail ), "destroy() on a %s context",
|
||||
status_name( ctx->status ) );
|
||||
beacon( "REFUSED", detail, aId );
|
||||
r.refusals++;
|
||||
return false;
|
||||
}
|
||||
|
||||
r.bytes -= ctx->c_stack.size() + ctx->asyncify_stack.size();
|
||||
r.live--;
|
||||
r.contexts.erase( aId );
|
||||
delete ctx;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
std::string stats_json()
|
||||
{
|
||||
Registry& r = reg();
|
||||
char buf[640];
|
||||
std::snprintf( buf, sizeof( buf ),
|
||||
"{\"live\":%zu,\"peakLive\":%zu,\"created\":%u,\"finished\":%u,"
|
||||
"\"transitions\":%u,\"refusals\":%u,\"running\":%u,"
|
||||
"\"transitionInFlight\":%s,\"readyQueued\":%zu,"
|
||||
"\"bytes\":%zu,\"peakBytes\":%zu,"
|
||||
"\"perContextBytes\":%zu,\"cStackBytes\":%zu,\"asyncifyBytes\":%zu,"
|
||||
"\"asyncifyHighWater\":%zu}",
|
||||
r.live, r.peak_live, r.created, r.finished,
|
||||
r.transitions, r.refusals, r.running,
|
||||
r.transition ? "true" : "false", r.ready_fifo.size(),
|
||||
r.bytes, r.peak_bytes,
|
||||
DEFAULT_C_STACK_BYTES + DEFAULT_ASYNCIFY_BYTES,
|
||||
DEFAULT_C_STACK_BYTES, DEFAULT_ASYNCIFY_BYTES,
|
||||
r.asyncify_high_water );
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
std::string registry_json()
|
||||
{
|
||||
std::string out = "[";
|
||||
bool first = true;
|
||||
|
||||
for( const auto& [id, ctx] : reg().contexts )
|
||||
{
|
||||
char entry[256];
|
||||
std::snprintf( entry, sizeof( entry ),
|
||||
"%s{\"id\":%u,\"label\":\"%s\",\"status\":\"%s\",\"reason\":\"%s\","
|
||||
"\"parks\":%u,\"resumes\":%u,\"asyncifyHighWater\":%zu}",
|
||||
first ? "" : ",", id, ctx->label, status_name( ctx->status ),
|
||||
ctx->park_reason, ctx->parks, ctx->resumes,
|
||||
ctx->asyncify_high_water );
|
||||
out += entry;
|
||||
first = false;
|
||||
}
|
||||
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
void reset_stats()
|
||||
{
|
||||
Registry& r = reg();
|
||||
r.created = 0;
|
||||
r.finished = 0;
|
||||
r.transitions = 0;
|
||||
r.refusals = 0;
|
||||
r.peak_live = r.live;
|
||||
r.peak_bytes = r.bytes;
|
||||
r.asyncify_high_water = 0;
|
||||
}
|
||||
|
||||
} // namespace pcbjam_sched
|
||||
126
wasm/sched/context.h
Normal file
126
wasm/sched/context.h
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Scheduler contexts — Design B core, phase D1
|
||||
* (pcbjam docs/features/async/20-design-b-core-plan.md §5, §6 D1).
|
||||
*
|
||||
* THE RULE THIS EXISTS TO ENFORCE: no activity may park "in place". A
|
||||
* suspendable activity runs on a scheduler-owned context (its own stack + its
|
||||
* own asyncify buffer) and suspends by YIELDING THAT CONTEXT back to the
|
||||
* scheduler. The registry below is then authoritative about what is parked and
|
||||
* why — never a fiber struct saying one thing while its body sits in a
|
||||
* handleSleep the fiber layer cannot see (docs/features/async/19).
|
||||
*
|
||||
* HOW THIS DIFFERS FROM libcontext (kicad/thirdparty/libcontext), which stays
|
||||
* exactly as it is:
|
||||
*
|
||||
* - libcontext is SYMMETRIC: any stack may jump_fcontext to any other, so
|
||||
* "is the target safe to enter?" has no recorded answer and is guessed
|
||||
* (swap_suspended, the parked/hot-main refusals). This layer is a STAR:
|
||||
* contexts only ever swap OUT to the scheduler, and only the scheduler
|
||||
* swaps IN. A resume is therefore never a guess — the registry says the
|
||||
* context is Parked/Ready and holds its buffer.
|
||||
* - At most ONE transition is in flight, enforced here rather than hoped for.
|
||||
*
|
||||
* SCOPE (D1): primitives + registry + memory accounting, exercised only by
|
||||
* tests/apps/standalone/sched-context. NO production path is switched to this
|
||||
* yet — dispatch moves at D2, waits at D3, bridges at D4.
|
||||
*
|
||||
* THREADING: main thread only, by construction (doc 21 §2 — every Asyncify
|
||||
* park in the tree is main-thread; the lib bridge's worker path is a blocking
|
||||
* proxy, not a park). Calling any of this from a pthread is a programming
|
||||
* error and is refused loudly.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace pcbjam_sched
|
||||
{
|
||||
|
||||
using ContextId = uint32_t;
|
||||
|
||||
/** Registry truth for one context. Only the scheduler mutates it. */
|
||||
enum class Status
|
||||
{
|
||||
Fresh, ///< created, never entered
|
||||
Running, ///< currently executing (at most one, plus the scheduler)
|
||||
Parked, ///< yielded, waiting for mark_ready()
|
||||
Ready, ///< mark_ready() called, waiting for drain() to swap it in
|
||||
Finished ///< entry returned; stack/buffer reclaimable
|
||||
};
|
||||
|
||||
const char* status_name( Status aStatus );
|
||||
|
||||
/** Per-context sizes. Both are charged to the memory budget (doc 20 risk 1). */
|
||||
struct Sizes
|
||||
{
|
||||
size_t stack_bytes = 0;
|
||||
size_t asyncify_bytes = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a context. It does NOT run until drain() picks it up: creation
|
||||
* marks it Ready so the first drain() enters it at aEntry.
|
||||
*
|
||||
* aLabel is borrowed for the context's lifetime (use a literal); it exists so
|
||||
* a stuck context can be named in a beacon instead of only numbered.
|
||||
* Returns 0 if the limit is hit or allocation fails.
|
||||
*/
|
||||
ContextId create( void ( *aEntry )( void* ), void* aArg, const char* aLabel );
|
||||
|
||||
/**
|
||||
* Park the CURRENTLY RUNNING context and yield to the scheduler. Returns the
|
||||
* result passed to mark_ready(). Must be called on a context (never on the
|
||||
* scheduler stack) — returns -1 immediately otherwise.
|
||||
*
|
||||
* aReason is recorded in the registry: "why is this parked" is exactly the
|
||||
* question the doc-19 guessing layer could not answer.
|
||||
*/
|
||||
int yield_park( const char* aReason );
|
||||
|
||||
/**
|
||||
* Mark a Parked context Ready with aResult. Callable from any stack (a JS
|
||||
* promise settling, a timer, another context). Does NOT resume it — resuming
|
||||
* is drain()'s job, so a wake can never rewind inside another transition
|
||||
* (doc 13 §1.4's deferred-wake law, applied to contexts).
|
||||
*
|
||||
* Returns false if the id is unknown or the context is not Parked.
|
||||
*/
|
||||
bool mark_ready( ContextId aId, int aResult );
|
||||
|
||||
/**
|
||||
* Scheduler entry: resume at most ONE Ready context, running it until it
|
||||
* parks or finishes. Returns the id it ran, or 0 if there was nothing to run
|
||||
* (or a transition was already in flight). Call from a clean stack — a fresh
|
||||
* JS task, never from inside an awaited export (#13302, doc 17 S3).
|
||||
*/
|
||||
ContextId drain();
|
||||
|
||||
/** True while a swap is in flight; drain() refuses to start another. */
|
||||
bool transition_in_flight();
|
||||
|
||||
/** The running context's id, or 0 when the scheduler stack is running. */
|
||||
ContextId current();
|
||||
|
||||
Status status_of( ContextId aId );
|
||||
|
||||
/** Destroy a Finished context and release its stack + buffer. */
|
||||
bool destroy( ContextId aId );
|
||||
|
||||
/**
|
||||
* Registry + memory snapshot as JSON, for tests and the D1 memory gate:
|
||||
* live/peakLive/created/finished, transitions, bytes/peakBytes,
|
||||
* perContextBytes, and asyncify high-water usage (asyncifyHighWater) —
|
||||
* the measurement doc 20 risk 1 asks for so buffer sizes get re-derived
|
||||
* from evidence instead of inherited from libcontext's 512K.
|
||||
*/
|
||||
std::string stats_json();
|
||||
|
||||
/** One line per live context: id, label, status, reason, asyncify usage. */
|
||||
std::string registry_json();
|
||||
|
||||
/** Test hook: reset all counters (does not touch live contexts). */
|
||||
void reset_stats();
|
||||
|
||||
} // namespace pcbjam_sched
|
||||
Loading…
Reference in a new issue