@intelligems/sst 2.49.6-ig.4 → 2.49.6-ig.6
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/cli/commands/dev.js +66 -1
- package/cli/sst.js +0 -1
- package/iot.js +30 -0
- package/package.json +2 -2
- package/package.json.bak +2 -2
- package/runtime/debug-bridge-logging.d.ts +24 -0
- package/runtime/debug-bridge-logging.js +98 -0
- package/runtime/debug-file-logger.d.ts +80 -0
- package/runtime/debug-file-logger.js +148 -0
- package/runtime/event-trace-logging.d.ts +27 -0
- package/runtime/event-trace-logging.js +69 -0
- package/runtime/handlers/node.js +8 -9
- package/runtime/handlers.d.ts +2 -0
- package/runtime/handlers.js +110 -65
- package/runtime/iot.js +23 -3
- package/runtime/mono-build-config.d.ts +55 -0
- package/runtime/mono-build-config.js +80 -0
- package/runtime/request-utils.d.ts +21 -0
- package/runtime/request-utils.js +102 -0
- package/runtime/runtime.d.ts +1 -0
- package/runtime/server.js +54 -7
- package/runtime/worker-pool-logging.js +65 -70
- package/runtime/workers.d.ts +26 -0
- package/runtime/workers.js +213 -16
- package/support/bridge/live-lambda.mjs +38 -38
- package/support/nodejs-runtime/index.mjs +17 -4
- package/support/python-runtime/runtime.py +20 -20
package/runtime/workers.js
CHANGED
|
@@ -8,6 +8,10 @@ import { useFunctions } from "../constructs/Function.js";
|
|
|
8
8
|
import { lazy } from "../util/lazy.js";
|
|
9
9
|
import { Logger } from "../logger.js";
|
|
10
10
|
import { POOL_SIZE, IDLE_TIMEOUT, logPool, logInvokeTrace, trackRequestStart, trackRequestEnd, setFunctionNameResolver, writeSessionEndSummary, } from "./worker-pool-logging.js";
|
|
11
|
+
import { useMonoBuildConfig, isMonoBuildPath } from "./mono-build-config.js";
|
|
12
|
+
import { getRequestPath, getCorrelationId, getApiGatewayRequestId } from "./request-utils.js";
|
|
13
|
+
import { logWorkers } from "./debug-bridge-logging.js";
|
|
14
|
+
import { logEventTrace } from "./event-trace-logging.js";
|
|
11
15
|
// Track workers marked as stale (should not return to pool after completion)
|
|
12
16
|
const staleWorkers = new Set();
|
|
13
17
|
const bundleMtimes = new Map();
|
|
@@ -23,7 +27,7 @@ process.on("exit", () => {
|
|
|
23
27
|
});
|
|
24
28
|
// Get bundle rebuild timestamp for staleness checking
|
|
25
29
|
function getBundleMtime(buildOut) {
|
|
26
|
-
if (buildOut
|
|
30
|
+
if (isMonoBuildPath(buildOut)) {
|
|
27
31
|
if (bundleMtimes.has(buildOut)) {
|
|
28
32
|
return bundleMtimes.get(buildOut);
|
|
29
33
|
}
|
|
@@ -72,11 +76,8 @@ function getBundleMtime(buildOut) {
|
|
|
72
76
|
// Helper: Get pool key for worker lookup
|
|
73
77
|
// For mono-build, uses shared key so any warm worker can serve any handler
|
|
74
78
|
function getPoolKey(functionID, runtime, buildOut) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return { key: `${runtime}:mono-build`, isShared: true };
|
|
78
|
-
}
|
|
79
|
-
return { key: `${runtime}:${functionID}`, isShared: false };
|
|
79
|
+
// Use the global mono build config for pool key calculation
|
|
80
|
+
return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
|
|
80
81
|
}
|
|
81
82
|
// Extract readable function name from handler path
|
|
82
83
|
function getFunctionName(functionID) {
|
|
@@ -318,9 +319,9 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
318
319
|
const props = useFunctions().fromID(functionID);
|
|
319
320
|
if (!props)
|
|
320
321
|
return;
|
|
321
|
-
// Get build to check if mono-build
|
|
322
|
+
// Get build to check if mono-build using global config
|
|
322
323
|
const build = await builder.artifact(functionID);
|
|
323
|
-
const isMonoBuild = build
|
|
324
|
+
const isMonoBuild = build ? isMonoBuildPath(build.out) : false;
|
|
324
325
|
if (isMonoBuild) {
|
|
325
326
|
// For mono-build: clear the entire shared pool since all functions share the same bundle
|
|
326
327
|
const sharedPoolKey = `${props.runtime}:mono-build`;
|
|
@@ -391,14 +392,28 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
391
392
|
const lastRequestId = new Map();
|
|
392
393
|
// Main invocation handler
|
|
393
394
|
bus.subscribe("function.invoked", async (evt) => {
|
|
394
|
-
const { workerID: awsWorkerID, functionID, requestID, env, } = evt.properties;
|
|
395
|
+
const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
|
|
395
396
|
const startTime = Date.now();
|
|
396
|
-
|
|
397
|
+
const requestPath = getRequestPath(event);
|
|
398
|
+
// Check if this is a warmup request - force-create new workers for these
|
|
399
|
+
// Matches the warmer format: { ding: true } or { warmer: true }
|
|
400
|
+
const isWarmupRequest = event && typeof event === 'object' &&
|
|
401
|
+
('ding' in event || 'warmer' in event || event.__sst_warmup === true);
|
|
402
|
+
const warmupId = isWarmupRequest ? (event.warmupId ?? event.index) : undefined;
|
|
403
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} RECEIVED func=${functionID.slice(-30)}`);
|
|
404
|
+
if (isWarmupRequest) {
|
|
405
|
+
logInvokeTrace("WARMUP_RECEIVED", requestID, `warmupId=${warmupId}`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
logInvokeTrace("INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
|
|
409
|
+
}
|
|
397
410
|
// Send ack immediately
|
|
398
|
-
bus.publish("function.ack", { functionID, workerID: awsWorkerID });
|
|
411
|
+
bus.publish("function.ack", { functionID, workerID: awsWorkerID, requestID });
|
|
412
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ACK sent elapsed=${Date.now() - startTime}ms`);
|
|
399
413
|
logInvokeTrace("ACK_PUBLISHED", requestID, `elapsed=${Date.now() - startTime}ms`);
|
|
400
414
|
const props = useFunctions().fromID(functionID);
|
|
401
415
|
if (!props) {
|
|
416
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Function not found`);
|
|
402
417
|
Logger.debug("Function not found:", functionID);
|
|
403
418
|
bus.publish("function.error", {
|
|
404
419
|
workerID: awsWorkerID,
|
|
@@ -412,6 +427,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
412
427
|
}
|
|
413
428
|
const handler = handlers.for(props.runtime);
|
|
414
429
|
if (!handler) {
|
|
430
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: No handler for runtime ${props.runtime}`);
|
|
415
431
|
Logger.debug("No handler for runtime:", props.runtime);
|
|
416
432
|
bus.publish("function.error", {
|
|
417
433
|
workerID: awsWorkerID,
|
|
@@ -423,10 +439,15 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
423
439
|
});
|
|
424
440
|
return;
|
|
425
441
|
}
|
|
442
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
|
|
426
443
|
logInvokeTrace("BUILD_ARTIFACT_START", requestID);
|
|
444
|
+
const buildStartTime = Date.now();
|
|
427
445
|
const build = await builder.artifact(functionID);
|
|
446
|
+
const buildElapsed = Date.now() - buildStartTime;
|
|
447
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Build artifact took ${buildElapsed}ms`);
|
|
428
448
|
logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
|
|
429
449
|
if (!build) {
|
|
450
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
|
|
430
451
|
Logger.debug("Build artifact not ready for:", functionID);
|
|
431
452
|
bus.publish("function.error", {
|
|
432
453
|
workerID: awsWorkerID,
|
|
@@ -444,13 +465,16 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
444
465
|
// === POOLED PATH ===
|
|
445
466
|
// Get pool key: shared for mono-build, per-function otherwise
|
|
446
467
|
const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, build.out);
|
|
447
|
-
|
|
468
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Looking for pooled worker, poolKey=${poolKey.slice(0, 20)}`);
|
|
469
|
+
// For warmup requests, always create new workers (never reuse from pool)
|
|
470
|
+
let pooledWorker = isWarmupRequest ? undefined : getIdleWorker(poolKey, functionID, build.out);
|
|
448
471
|
let isReuse = false;
|
|
449
472
|
if (pooledWorker) {
|
|
450
473
|
isReuse = true;
|
|
451
474
|
// Update functionID for cross-function reuse (mono-build)
|
|
452
475
|
pooledWorker.functionID = functionID;
|
|
453
476
|
trackRequestStart(functionID, true);
|
|
477
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} REUSING pooled worker ${pooledWorker.pooledWorkerID.slice(0, 8)}`);
|
|
454
478
|
}
|
|
455
479
|
else {
|
|
456
480
|
// Create new pooled worker
|
|
@@ -466,6 +490,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
466
490
|
bundlePath: build.out,
|
|
467
491
|
bundleMtime,
|
|
468
492
|
};
|
|
493
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} CREATING new pooled worker ${pooledWorkerID.slice(0, 8)}`);
|
|
469
494
|
}
|
|
470
495
|
// Set up mappings
|
|
471
496
|
workerIDMapping.set(awsWorkerID, pooledWorker.pooledWorkerID);
|
|
@@ -476,7 +501,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
476
501
|
// Start new worker with pooledWorkerID (cold start)
|
|
477
502
|
trackRequestStart(functionID, false);
|
|
478
503
|
const currentPoolSize = workerPool.get(poolKey)?.length || 0;
|
|
479
|
-
logPool("CREATE", {
|
|
504
|
+
logPool(isWarmupRequest ? "WARMUP_CREATE" : "CREATE", {
|
|
480
505
|
pooledWorkerID: pooledWorker.pooledWorkerID.slice(0, 8),
|
|
481
506
|
functionID,
|
|
482
507
|
runtime: props.runtime,
|
|
@@ -485,8 +510,11 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
485
510
|
isSharedPool: isShared,
|
|
486
511
|
currentPoolSize,
|
|
487
512
|
activeWorkers: activeWorkers.size,
|
|
513
|
+
...(isWarmupRequest && { warmupId }),
|
|
488
514
|
});
|
|
515
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${pooledWorker.pooledWorkerID.slice(0, 8)}...`);
|
|
489
516
|
logInvokeTrace("WORKER_START", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
|
|
517
|
+
const workerStartTime = Date.now();
|
|
490
518
|
try {
|
|
491
519
|
await handler.startWorker({
|
|
492
520
|
...build,
|
|
@@ -495,15 +523,29 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
495
523
|
environment: env,
|
|
496
524
|
url: `${serverConfig.url}/${pooledWorker.pooledWorkerID}/${serverConfig.API_VERSION}`,
|
|
497
525
|
runtime: props.runtime,
|
|
526
|
+
isMonoBuild: isShared,
|
|
498
527
|
});
|
|
499
528
|
startedWorkers.add(pooledWorker.pooledWorkerID);
|
|
529
|
+
const workerStartElapsed = Date.now() - workerStartTime;
|
|
530
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker started in ${workerStartElapsed}ms`);
|
|
500
531
|
logInvokeTrace("WORKER_STARTED", requestID);
|
|
532
|
+
logEventTrace("WORKER_START", {
|
|
533
|
+
requestID,
|
|
534
|
+
functionID,
|
|
535
|
+
workerID: pooledWorker.pooledWorkerID,
|
|
536
|
+
path: requestPath,
|
|
537
|
+
correlationId: getCorrelationId(event),
|
|
538
|
+
apiGwReqId: getApiGatewayRequestId(event),
|
|
539
|
+
elapsed: workerStartElapsed,
|
|
540
|
+
reused: false,
|
|
541
|
+
});
|
|
501
542
|
bus.publish("worker.started", {
|
|
502
543
|
workerID: awsWorkerID,
|
|
503
544
|
functionID,
|
|
504
545
|
});
|
|
505
546
|
}
|
|
506
547
|
catch (ex) {
|
|
548
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
|
|
507
549
|
Logger.debug("Failed to start pooled worker", ex);
|
|
508
550
|
bus.publish("function.error", {
|
|
509
551
|
workerID: awsWorkerID,
|
|
@@ -524,6 +566,15 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
524
566
|
}
|
|
525
567
|
else {
|
|
526
568
|
logInvokeTrace("WORKER_REUSE", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
|
|
569
|
+
logEventTrace("WORKER_START", {
|
|
570
|
+
requestID,
|
|
571
|
+
functionID,
|
|
572
|
+
workerID: pooledWorker.pooledWorkerID,
|
|
573
|
+
path: requestPath,
|
|
574
|
+
correlationId: getCorrelationId(event),
|
|
575
|
+
apiGwReqId: getApiGatewayRequestId(event),
|
|
576
|
+
reused: true,
|
|
577
|
+
});
|
|
527
578
|
bus.publish("worker.reused", {
|
|
528
579
|
workerID: awsWorkerID,
|
|
529
580
|
functionID,
|
|
@@ -532,6 +583,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
532
583
|
}
|
|
533
584
|
// Route invocation to the pooled worker
|
|
534
585
|
const server = await getServer();
|
|
586
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Routing invocation to worker ${pooledWorker.pooledWorkerID.slice(0, 8)} elapsed=${Date.now() - startTime}ms`);
|
|
535
587
|
logInvokeTrace("ROUTE_INVOCATION", requestID);
|
|
536
588
|
server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
|
|
537
589
|
}
|
|
@@ -549,6 +601,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
549
601
|
environment: env,
|
|
550
602
|
url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
|
|
551
603
|
runtime: props.runtime,
|
|
604
|
+
isMonoBuild: isMonoBuildPath(build.out),
|
|
552
605
|
});
|
|
553
606
|
workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
|
|
554
607
|
bus.publish("worker.started", { workerID: awsWorkerID, functionID });
|
|
@@ -598,23 +651,49 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
598
651
|
if (pooled) {
|
|
599
652
|
const requestID = lastRequestId.get(workerID);
|
|
600
653
|
if (requestID) {
|
|
654
|
+
const trimmedMessage = message.trim();
|
|
655
|
+
// Log messages that contain [LOG] prefix
|
|
656
|
+
if (trimmedMessage.includes("[LOG]")) {
|
|
657
|
+
logEventTrace("WORKER_LOG", {
|
|
658
|
+
requestID,
|
|
659
|
+
functionID: pooled.functionID,
|
|
660
|
+
workerID,
|
|
661
|
+
message: trimmedMessage,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
601
664
|
bus.publish("worker.stdout", {
|
|
602
665
|
workerID,
|
|
603
666
|
functionID: pooled.functionID,
|
|
604
|
-
message:
|
|
667
|
+
message: trimmedMessage,
|
|
605
668
|
requestID,
|
|
606
669
|
});
|
|
607
670
|
}
|
|
608
671
|
return;
|
|
609
672
|
}
|
|
673
|
+
// Check if this is a preWarm worker (started but not yet active)
|
|
674
|
+
if (startedWorkers.has(workerID)) {
|
|
675
|
+
// During preWarm, ignore output since there's no request context
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
610
678
|
// Non-pooled worker
|
|
611
679
|
const worker = workers.get(workerID);
|
|
612
680
|
if (!worker)
|
|
613
681
|
return;
|
|
682
|
+
const trimmedMessage = message.trim();
|
|
683
|
+
const requestID = lastRequestId.get(workerID);
|
|
684
|
+
// Log messages that contain [LOG] prefix
|
|
685
|
+
if (trimmedMessage.includes("[LOG]") && requestID) {
|
|
686
|
+
logEventTrace("WORKER_LOG", {
|
|
687
|
+
requestID,
|
|
688
|
+
functionID: worker.functionID,
|
|
689
|
+
workerID,
|
|
690
|
+
message: trimmedMessage,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
614
693
|
bus.publish("worker.stdout", {
|
|
615
694
|
...worker,
|
|
616
|
-
message:
|
|
617
|
-
requestID:
|
|
695
|
+
message: trimmedMessage,
|
|
696
|
+
requestID: requestID,
|
|
618
697
|
});
|
|
619
698
|
},
|
|
620
699
|
exited(workerID) {
|
|
@@ -677,5 +756,123 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
677
756
|
return activeWorkers.has(workerID) || startedWorkers.has(workerID);
|
|
678
757
|
},
|
|
679
758
|
subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
|
|
759
|
+
/**
|
|
760
|
+
* Trigger warmup by invoking Lambda functions with warmup payloads.
|
|
761
|
+
* This sends real requests through the IoT bridge, which naturally creates workers.
|
|
762
|
+
* @param count Number of workers to warm up (default: 15)
|
|
763
|
+
*/
|
|
764
|
+
async triggerWarmup(count = 15) {
|
|
765
|
+
const functions = useFunctions();
|
|
766
|
+
const allFunctions = functions.all;
|
|
767
|
+
// Find a nodejs function to use as the warmup target
|
|
768
|
+
let targetFunction = null;
|
|
769
|
+
for (const [id, props] of Object.entries(allFunctions)) {
|
|
770
|
+
if (!props.runtime?.startsWith("nodejs"))
|
|
771
|
+
continue;
|
|
772
|
+
if (!isPoolableRuntime(props.runtime))
|
|
773
|
+
continue;
|
|
774
|
+
if (!props.functionName)
|
|
775
|
+
continue;
|
|
776
|
+
targetFunction = { id, props, functionName: props.functionName };
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
if (!targetFunction) {
|
|
780
|
+
logPool("WARMUP_SKIP", {
|
|
781
|
+
reason: "no nodejs function found",
|
|
782
|
+
});
|
|
783
|
+
return { warmed: 0 };
|
|
784
|
+
}
|
|
785
|
+
const { functionName } = targetFunction;
|
|
786
|
+
logPool("WARMUP_START", {
|
|
787
|
+
count,
|
|
788
|
+
functionName,
|
|
789
|
+
});
|
|
790
|
+
// Publish warmup start event
|
|
791
|
+
bus.publish("warmup.start", { count });
|
|
792
|
+
const startTime = Date.now();
|
|
793
|
+
let success = 0;
|
|
794
|
+
let failed = 0;
|
|
795
|
+
let completed = 0;
|
|
796
|
+
// Use the shared AWS client
|
|
797
|
+
const { useAWSClient } = await import("../credentials.js");
|
|
798
|
+
const { LambdaClient, InvokeCommand } = await import("@aws-sdk/client-lambda");
|
|
799
|
+
const lambda = useAWSClient(LambdaClient);
|
|
800
|
+
// Helper to publish progress
|
|
801
|
+
const publishProgress = () => {
|
|
802
|
+
bus.publish("warmup.progress", {
|
|
803
|
+
completed,
|
|
804
|
+
total: count,
|
|
805
|
+
success,
|
|
806
|
+
failed,
|
|
807
|
+
});
|
|
808
|
+
};
|
|
809
|
+
// Phase 1: Invoke first warmup to populate V8 compile cache
|
|
810
|
+
try {
|
|
811
|
+
const result = await lambda.send(new InvokeCommand({
|
|
812
|
+
FunctionName: functionName,
|
|
813
|
+
InvocationType: "RequestResponse",
|
|
814
|
+
Payload: JSON.stringify({
|
|
815
|
+
ding: true,
|
|
816
|
+
concurrency: count,
|
|
817
|
+
index: 0,
|
|
818
|
+
}),
|
|
819
|
+
}));
|
|
820
|
+
if (result.StatusCode === 200) {
|
|
821
|
+
success++;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
failed++;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch (ex) {
|
|
828
|
+
failed++;
|
|
829
|
+
}
|
|
830
|
+
completed++;
|
|
831
|
+
publishProgress();
|
|
832
|
+
// Phase 2: Invoke remaining warmups in parallel
|
|
833
|
+
if (count > 1) {
|
|
834
|
+
const results = await Promise.all(Array.from({ length: count - 1 }, (_, i) => i + 1).map(async (i) => {
|
|
835
|
+
try {
|
|
836
|
+
const result = await lambda.send(new InvokeCommand({
|
|
837
|
+
FunctionName: functionName,
|
|
838
|
+
InvocationType: "RequestResponse",
|
|
839
|
+
Payload: JSON.stringify({
|
|
840
|
+
ding: true,
|
|
841
|
+
concurrency: count,
|
|
842
|
+
index: i,
|
|
843
|
+
}),
|
|
844
|
+
}));
|
|
845
|
+
const ok = result.StatusCode === 200;
|
|
846
|
+
if (ok)
|
|
847
|
+
success++;
|
|
848
|
+
else
|
|
849
|
+
failed++;
|
|
850
|
+
completed++;
|
|
851
|
+
publishProgress();
|
|
852
|
+
return ok;
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
failed++;
|
|
856
|
+
completed++;
|
|
857
|
+
publishProgress();
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
const elapsed = Date.now() - startTime;
|
|
863
|
+
logPool("WARMUP_DONE", {
|
|
864
|
+
success,
|
|
865
|
+
failed,
|
|
866
|
+
elapsedMs: elapsed,
|
|
867
|
+
avgMs: success > 0 ? Math.round(elapsed / success) : 0,
|
|
868
|
+
});
|
|
869
|
+
// Publish warmup complete event
|
|
870
|
+
bus.publish("warmup.complete", {
|
|
871
|
+
success,
|
|
872
|
+
failed,
|
|
873
|
+
elapsedMs: elapsed,
|
|
874
|
+
});
|
|
875
|
+
return { warmed: success, elapsed };
|
|
876
|
+
},
|
|
680
877
|
};
|
|
681
878
|
});
|