1tube 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +1 -1
- package/src/backends/deno/dep-graph.ts +40 -2
- package/src/backends/deno/worker-host.ts +53 -2
- package/src/logs/query.ts +25 -1
- package/src/server.ts +15 -0
package/README.md
CHANGED
|
@@ -120,7 +120,12 @@ Deferred boot (default whenever `--dev` or `--hmr` is on) flips the order:
|
|
|
120
120
|
the gateway starts serving right away. Time-to-first-request drops from
|
|
121
121
|
"sum of all worker spawns" to effectively zero.
|
|
122
122
|
2. Workers spawn in the background with the usual bounded concurrency
|
|
123
|
-
(`--deno-worker-concurrency`, default 8)
|
|
123
|
+
(`--deno-worker-concurrency`, default 8), **most-recently-used first**:
|
|
124
|
+
the queue is ordered by each function's last invocation time from the
|
|
125
|
+
invocation log store, so the functions you were actually hitting before
|
|
126
|
+
the restart are warm within the first few seconds and the long tail of
|
|
127
|
+
rarely-called functions loads last. First run (empty store) falls back
|
|
128
|
+
to name order.
|
|
124
129
|
3. A request for a function whose Worker isn't ready yet **bumps it to the
|
|
125
130
|
front of the queue** and spawns it immediately, out-of-band. If it becomes
|
|
126
131
|
ready within the grace window (`--warmup-grace-ms`, default 250 ms) the
|
|
@@ -190,6 +195,15 @@ little as possible:
|
|
|
190
195
|
`X-1tube-Stale: 1` (CORS-exposed) so tooling can tell the difference.
|
|
191
196
|
- **Targeted re-discovery.** Reloading one function stats only that function's
|
|
192
197
|
dir instead of re-scanning the whole functions root.
|
|
198
|
+
- **Local-only dep graphs.** The module graph that maps file changes to
|
|
199
|
+
affected functions stops at the local-file boundary: `npm:`/`jsr:`/`https:`
|
|
200
|
+
subtrees are marked external instead of being fetched and parsed. Only
|
|
201
|
+
`file://` modules can fire watcher events, so nothing is lost — and graph
|
|
202
|
+
builds for npm-heavy functions drop from seconds to milliseconds.
|
|
203
|
+
- **One batch graph per deferred boot.** With shared modules active, deferred
|
|
204
|
+
boot builds a single module graph for all functions in the background
|
|
205
|
+
instead of letting every spawn crawl its own (which re-parsed the same
|
|
206
|
+
`_shared` files once per function).
|
|
193
207
|
|
|
194
208
|
Notes:
|
|
195
209
|
|
package/package.json
CHANGED
|
@@ -164,6 +164,38 @@ function resolveAgainst(target: string, base: string): string {
|
|
|
164
164
|
return new URL(target, baseUrl).href;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Local-only module loader for `createGraph`.
|
|
169
|
+
*
|
|
170
|
+
* The graph exists solely to map fs-watcher events to affected
|
|
171
|
+
* functions, and only `file://` modules can ever fire those events.
|
|
172
|
+
* deno_graph's DEFAULT loader, however, follows every specifier —
|
|
173
|
+
* reading npm/jsr packages and even `fetch()`ing `https:` imports —
|
|
174
|
+
* which made big functions pay multi-second graph builds for data we
|
|
175
|
+
* throw away. Marking every non-`file:` specifier as external stops
|
|
176
|
+
* the crawl at the local-file boundary: remote subtrees are never
|
|
177
|
+
* read, parsed, or fetched.
|
|
178
|
+
*/
|
|
179
|
+
async function loadLocalOnly(
|
|
180
|
+
specifier: string,
|
|
181
|
+
): Promise<
|
|
182
|
+
| { kind: "module"; specifier: string; content: Uint8Array }
|
|
183
|
+
| { kind: "external"; specifier: string }
|
|
184
|
+
| undefined
|
|
185
|
+
> {
|
|
186
|
+
if (!specifier.startsWith("file://")) {
|
|
187
|
+
return { kind: "external", specifier };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const content = await Deno.readFile(fileURLToPath(specifier));
|
|
191
|
+
return { kind: "module", specifier, content };
|
|
192
|
+
} catch {
|
|
193
|
+
// Missing file mid-edit: report "not found" and let deno_graph
|
|
194
|
+
// record a load error for this module instead of throwing.
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
167
199
|
export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
168
200
|
const perName = new Map<string, Set<string>>();
|
|
169
201
|
const reverse = new Map<string, Set<string>>();
|
|
@@ -247,7 +279,10 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
247
279
|
const entry = normalize(entryFileUrl);
|
|
248
280
|
let graph;
|
|
249
281
|
try {
|
|
250
|
-
graph = await createGraph([entry],
|
|
282
|
+
graph = await createGraph([entry], {
|
|
283
|
+
load: loadLocalOnly,
|
|
284
|
+
...(resolve ? { resolve } : {}),
|
|
285
|
+
});
|
|
251
286
|
} catch {
|
|
252
287
|
// A save mid-flush can leave a syntactically broken file on
|
|
253
288
|
// disk; deno_graph throws. Drop the graph for `name` so the
|
|
@@ -281,7 +316,10 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
|
|
|
281
316
|
try {
|
|
282
317
|
graph = await createGraph(
|
|
283
318
|
normalized.map((e) => e.entry),
|
|
284
|
-
|
|
319
|
+
{
|
|
320
|
+
load: loadLocalOnly,
|
|
321
|
+
...(resolve ? { resolve } : {}),
|
|
322
|
+
},
|
|
285
323
|
);
|
|
286
324
|
} catch {
|
|
287
325
|
for (const e of normalized) replaceGraph(e.name, minimalGraph(e.entry));
|
|
@@ -93,6 +93,14 @@ export interface DenoWorkerHostOptions {
|
|
|
93
93
|
captureConsole?: boolean;
|
|
94
94
|
/** Sink for captured console lines + worker runtime errors. */
|
|
95
95
|
onConsole?: (event: WorkerConsoleEvent) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Last-dispatch timestamp per function from a PREVIOUS run (e.g. the
|
|
98
|
+
* invocation log store). Seeds the MRU ordering used by deferred
|
|
99
|
+
* boot and HMR respawns, so the functions the developer was hitting
|
|
100
|
+
* before a gateway restart warm up first instead of alphabetically.
|
|
101
|
+
* Fresh in-process dispatches overwrite the seed as traffic arrives.
|
|
102
|
+
*/
|
|
103
|
+
initialRecency?: ReadonlyMap<string, number>;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
export interface ReloadSummary {
|
|
@@ -595,7 +603,7 @@ export function createDenoWorkerHost(
|
|
|
595
603
|
/** Spawns started out-of-band by prioritize(), above the lane bound. */
|
|
596
604
|
const outOfBandSpawns = new Set<Promise<void>>();
|
|
597
605
|
/** Last gateway dispatch per function — drives MRU reload ordering. */
|
|
598
|
-
const lastDispatch = new Map<string, number>();
|
|
606
|
+
const lastDispatch = new Map<string, number>(opts.initialRecency ?? []);
|
|
599
607
|
/**
|
|
600
608
|
* Per-name spawn serialization. Deferred boot and HMR reload can both
|
|
601
609
|
* want to spawn the same function concurrently (e.g. an edit lands
|
|
@@ -955,6 +963,13 @@ export function createDenoWorkerHost(
|
|
|
955
963
|
loaded: string[];
|
|
956
964
|
errors: Array<{ name: string; error: string }>;
|
|
957
965
|
nextIndex: number;
|
|
966
|
+
/**
|
|
967
|
+
* Resolves when the boot-wide batch dep-graph (shared-rewrite mode
|
|
968
|
+
* only) has landed. Spawns wait on it instead of each paying a
|
|
969
|
+
* full per-function graph crawl — one batch graph parses every
|
|
970
|
+
* `_shared` file once instead of once per function.
|
|
971
|
+
*/
|
|
972
|
+
batchGraphReady: Promise<void> | null;
|
|
958
973
|
}
|
|
959
974
|
let deferredCtx: DeferredCtx | null = null;
|
|
960
975
|
|
|
@@ -967,7 +982,11 @@ export function createDenoWorkerHost(
|
|
|
967
982
|
const index = ++ctx.nextIndex;
|
|
968
983
|
ctx.startOpts.onSpawnStart?.({ index, total: ctx.total, name: cand.name });
|
|
969
984
|
const t0 = performance.now();
|
|
970
|
-
|
|
985
|
+
if (ctx.batchGraphReady) await ctx.batchGraphReady;
|
|
986
|
+
const r = await runSerialized(
|
|
987
|
+
cand.name,
|
|
988
|
+
() => spawnAndRegister(cand, { graphReady: ctx.batchGraphReady !== null }),
|
|
989
|
+
);
|
|
971
990
|
const durationMs = performance.now() - t0;
|
|
972
991
|
if ("error" in r) {
|
|
973
992
|
ctx.errors.push({ name: cand.name, error: r.error });
|
|
@@ -1003,12 +1022,44 @@ export function createDenoWorkerHost(
|
|
|
1003
1022
|
startOpts: StartOptions = {},
|
|
1004
1023
|
): Promise<DeferredStart> {
|
|
1005
1024
|
const candidates = await discoverCandidates(opts.functionsDir);
|
|
1025
|
+
// MRU-first warm-up: the background queue boots the functions the
|
|
1026
|
+
// developer was hitting most recently (seeded from the invocation
|
|
1027
|
+
// log store across restarts) before the ones nobody calls. Ties —
|
|
1028
|
+
// including everything on a first run — keep name order.
|
|
1029
|
+
candidates.sort((a, b) =>
|
|
1030
|
+
(lastDispatch.get(b.name) ?? 0) - (lastDispatch.get(a.name) ?? 0) ||
|
|
1031
|
+
a.name.localeCompare(b.name)
|
|
1032
|
+
);
|
|
1033
|
+
// Shared-rewrite mode needs each function's file list BEFORE its
|
|
1034
|
+
// worker spawns. Build ONE batch graph in the background (the
|
|
1035
|
+
// gateway still starts serving immediately) rather than letting
|
|
1036
|
+
// every spawn crawl its own graph — the per-function crawls
|
|
1037
|
+
// re-parse the same shared files over and over.
|
|
1038
|
+
const useSharedRewrite = Boolean(
|
|
1039
|
+
rewriteCache && sharedRuntime && sharedRuntime.list().length > 0,
|
|
1040
|
+
);
|
|
1041
|
+
let batchGraphReady: Promise<void> | null = null;
|
|
1042
|
+
if (useSharedRewrite && candidates.length > 0) {
|
|
1043
|
+
const graphStart = performance.now();
|
|
1044
|
+
batchGraphReady = depGraph
|
|
1045
|
+
.refreshMany(
|
|
1046
|
+
candidates.map((c) => ({ name: c.name, entryFileUrl: c.entryUrl })),
|
|
1047
|
+
)
|
|
1048
|
+
.then(() => {
|
|
1049
|
+
startOpts.onBatchGraph?.({
|
|
1050
|
+
total: candidates.length,
|
|
1051
|
+
durationMs: performance.now() - graphStart,
|
|
1052
|
+
});
|
|
1053
|
+
})
|
|
1054
|
+
.catch(() => {});
|
|
1055
|
+
}
|
|
1006
1056
|
const ctx: DeferredCtx = {
|
|
1007
1057
|
total: candidates.length,
|
|
1008
1058
|
startOpts,
|
|
1009
1059
|
loaded: [],
|
|
1010
1060
|
errors: [],
|
|
1011
1061
|
nextIndex: 0,
|
|
1062
|
+
batchGraphReady,
|
|
1012
1063
|
};
|
|
1013
1064
|
deferredCtx = ctx;
|
|
1014
1065
|
const items: ClaimItem[] = [];
|
package/src/logs/query.ts
CHANGED
|
@@ -195,6 +195,12 @@ export interface LogQuery {
|
|
|
195
195
|
logsSince(afterId: number, limit?: number): LogRecord[];
|
|
196
196
|
/** Distinct function names seen in the store (for filter dropdowns). */
|
|
197
197
|
functionNames(): string[];
|
|
198
|
+
/**
|
|
199
|
+
* Most recent invocation timestamp per function, across the whole
|
|
200
|
+
* retention window. Seeds the Deno worker host's MRU boot ordering so
|
|
201
|
+
* deferred boot warms the functions the developer actually uses first.
|
|
202
|
+
*/
|
|
203
|
+
lastDispatchByFunction(): Map<string, number>;
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
export function createLogQuery(db: LogDb): LogQuery {
|
|
@@ -368,5 +374,23 @@ export function createLogQuery(db: LogDb): LogQuery {
|
|
|
368
374
|
return rows.map((r) => r.function_name);
|
|
369
375
|
}
|
|
370
376
|
|
|
371
|
-
|
|
377
|
+
function lastDispatchByFunction(): Map<string, number> {
|
|
378
|
+
// Satisfied entirely by idx_invocations_fn_ts (fn, ts) — one index
|
|
379
|
+
// scan, no table reads.
|
|
380
|
+
const rows = raw.prepare(
|
|
381
|
+
`SELECT function_name, MAX(ts_ms) AS last_ts FROM invocations GROUP BY function_name`,
|
|
382
|
+
).all() as unknown as Array<{ function_name: string; last_ts: number }>;
|
|
383
|
+
const out = new Map<string, number>();
|
|
384
|
+
for (const r of rows) out.set(r.function_name, Number(r.last_ts));
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
queryInvocations,
|
|
390
|
+
getInvocation,
|
|
391
|
+
searchLogs,
|
|
392
|
+
logsSince,
|
|
393
|
+
functionNames,
|
|
394
|
+
lastDispatchByFunction,
|
|
395
|
+
};
|
|
372
396
|
}
|
package/src/server.ts
CHANGED
|
@@ -1329,6 +1329,18 @@ if (opts.backend === "workerd") {
|
|
|
1329
1329
|
);
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
|
+
// MRU warm-up seed: the invocation log store remembers which
|
|
1333
|
+
// functions were hit before the restart; deferred boot warms those
|
|
1334
|
+
// first instead of going alphabetically.
|
|
1335
|
+
let bootRecency: Map<string, number> | undefined;
|
|
1336
|
+
if (logDbHandle) {
|
|
1337
|
+
try {
|
|
1338
|
+
bootRecency = createLogQuery(logDbHandle).lastDispatchByFunction();
|
|
1339
|
+
} catch {
|
|
1340
|
+
// Best-effort: a broken store only costs the smarter boot order.
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1332
1344
|
denoWorkerHost = createDenoWorkerHost({
|
|
1333
1345
|
functionsDir: resolvedFunctionsPath,
|
|
1334
1346
|
registry,
|
|
@@ -1351,6 +1363,9 @@ if (opts.backend === "workerd") {
|
|
|
1351
1363
|
...(denoImportMapOptions ?? {}),
|
|
1352
1364
|
...(denoSharedRuntime ? { sharedRuntime: denoSharedRuntime } : {}),
|
|
1353
1365
|
...(denoRewriteCache ? { rewriteCache: denoRewriteCache } : {}),
|
|
1366
|
+
...(bootRecency && bootRecency.size > 0
|
|
1367
|
+
? { initialRecency: bootRecency }
|
|
1368
|
+
: {}),
|
|
1354
1369
|
});
|
|
1355
1370
|
|
|
1356
1371
|
// Boot progress: append-only `[i/N] ✓ name (123ms)` per worker plus
|