@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/server.js
CHANGED
|
@@ -2,9 +2,13 @@ import express from "express";
|
|
|
2
2
|
import { useBus } from "../bus.js";
|
|
3
3
|
import { Logger } from "../logger.js";
|
|
4
4
|
import { useRuntimeWorkers } from "./workers.js";
|
|
5
|
+
import { useFunctions } from "../constructs/Function.js";
|
|
5
6
|
import https from "https";
|
|
6
7
|
import getPort from "get-port";
|
|
7
8
|
import { lazy } from "../util/lazy.js";
|
|
9
|
+
import { getRequestPath } from "./request-utils.js";
|
|
10
|
+
import { logServer } from "./debug-bridge-logging.js";
|
|
11
|
+
import { logEventTrace } from "./event-trace-logging.js";
|
|
8
12
|
export const useRuntimeServerConfig = lazy(async () => {
|
|
9
13
|
const port = await getPort({
|
|
10
14
|
port: 12557,
|
|
@@ -33,9 +37,12 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
33
37
|
}
|
|
34
38
|
// Route an invocation to a specific workerID (used by workers.ts for pooled workers)
|
|
35
39
|
function routeInvocation(targetWorkerID, invocation) {
|
|
40
|
+
const requestPath = getRequestPath(invocation.event);
|
|
41
|
+
const requestID = invocation.requestID;
|
|
36
42
|
const waiting = workersWaiting.get(targetWorkerID);
|
|
37
43
|
if (waiting) {
|
|
38
44
|
workersWaiting.delete(targetWorkerID);
|
|
45
|
+
logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} was waiting, delivering immediately`);
|
|
39
46
|
waiting(invocation);
|
|
40
47
|
return;
|
|
41
48
|
}
|
|
@@ -45,6 +52,7 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
45
52
|
invocationsQueued.set(targetWorkerID, arr);
|
|
46
53
|
}
|
|
47
54
|
arr.push(invocation);
|
|
55
|
+
logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} not waiting, QUEUED (queueSize=${arr.length})`);
|
|
48
56
|
}
|
|
49
57
|
workers.subscribe("worker.exited", async (evt) => {
|
|
50
58
|
const waiting = workersWaiting.get(evt.properties.workerID);
|
|
@@ -69,8 +77,16 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
69
77
|
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
70
78
|
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
71
79
|
: pooledWorkerID;
|
|
80
|
+
const requestID = workers.getCurrentRequestID(pooledWorkerID);
|
|
81
|
+
logEventTrace("WORKER_END", {
|
|
82
|
+
requestID: requestID || "unknown",
|
|
83
|
+
functionID: worker.functionID,
|
|
84
|
+
workerID: pooledWorkerID,
|
|
85
|
+
status: "error",
|
|
86
|
+
errorType: req.body?.errorType || "init_error",
|
|
87
|
+
});
|
|
72
88
|
bus.publish("function.error", {
|
|
73
|
-
requestID
|
|
89
|
+
requestID,
|
|
74
90
|
workerID: awsWorkerID,
|
|
75
91
|
functionID: worker.functionID,
|
|
76
92
|
...req.body,
|
|
@@ -80,11 +96,20 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
80
96
|
res.json("ok");
|
|
81
97
|
});
|
|
82
98
|
app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Logger.debug("Worker",
|
|
99
|
+
const workerID = req.params.workerID;
|
|
100
|
+
logServer(`Worker ${workerID.slice(0, 8)} requesting /next`);
|
|
101
|
+
Logger.debug("Worker", workerID, "is waiting for next invocation");
|
|
102
|
+
const waitStart = Date.now();
|
|
103
|
+
const payload = await next(workerID);
|
|
104
|
+
const requestPath = getRequestPath(payload.event);
|
|
105
|
+
const requestID = payload.context.awsRequestId;
|
|
106
|
+
logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${workerID.slice(0, 8)} received payload after waiting ${Date.now() - waitStart}ms`);
|
|
107
|
+
Logger.debug("Worker", workerID, "sending next payload");
|
|
108
|
+
// Get function properties for per-invocation context
|
|
109
|
+
// This is essential for mono-build shared workers that serve multiple functions
|
|
110
|
+
const funcProps = useFunctions().fromID(payload.functionID);
|
|
86
111
|
res.set({
|
|
87
|
-
"Lambda-Runtime-Aws-Request-Id":
|
|
112
|
+
"Lambda-Runtime-Aws-Request-Id": requestID,
|
|
88
113
|
"Lambda-Runtime-Deadline-Ms": Date.now() + payload.deadline,
|
|
89
114
|
"Lambda-Runtime-Invoked-Function-Arn": payload.context.invokedFunctionArn,
|
|
90
115
|
"Lambda-Runtime-Client-Context": JSON.stringify(payload.context.clientContext || null),
|
|
@@ -93,6 +118,10 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
93
118
|
"Lambda-Runtime-Log-Stream-Name": payload.context.logStreamName,
|
|
94
119
|
// Pass function ID for mono-build shared pool: allows per-invocation dispatch
|
|
95
120
|
"Lambda-Runtime-Sst-Function-Id": payload.functionID,
|
|
121
|
+
// Per-invocation function context for mono-build shared workers
|
|
122
|
+
"Lambda-Runtime-Function-Name": funcProps?.functionName || "",
|
|
123
|
+
"Lambda-Runtime-Function-Version": "$LATEST",
|
|
124
|
+
"Lambda-Runtime-Function-Memory-Size": String(funcProps?.memorySize || 1024),
|
|
96
125
|
});
|
|
97
126
|
// Wrap event with env for per-invocation environment variable application
|
|
98
127
|
// This prevents env leakage when workers are reused across different functions
|
|
@@ -109,9 +138,12 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
109
138
|
limit: "10mb",
|
|
110
139
|
}), (req, res) => {
|
|
111
140
|
const pooledWorkerID = req.params.workerID;
|
|
141
|
+
const requestID = req.params.awsRequestId;
|
|
142
|
+
logServer(`reqId=${requestID.slice(0, 8)} Worker ${pooledWorkerID.slice(0, 8)} posting /response`);
|
|
112
143
|
Logger.debug("Worker", pooledWorkerID, "got response", req.body);
|
|
113
144
|
const worker = workers.fromID(pooledWorkerID);
|
|
114
145
|
if (!worker) {
|
|
146
|
+
logServer(`reqId=${requestID.slice(0, 8)} ERROR: Worker ${pooledWorkerID.slice(0, 8)} not found`);
|
|
115
147
|
res.status(404).send();
|
|
116
148
|
return;
|
|
117
149
|
}
|
|
@@ -119,10 +151,17 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
119
151
|
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
120
152
|
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
121
153
|
: pooledWorkerID;
|
|
154
|
+
logServer(`reqId=${requestID.slice(0, 8)} Publishing function.success awsWorkerID=${awsWorkerID.slice(0, 8)}`);
|
|
155
|
+
logEventTrace("WORKER_END", {
|
|
156
|
+
requestID,
|
|
157
|
+
functionID: worker.functionID,
|
|
158
|
+
workerID: pooledWorkerID,
|
|
159
|
+
status: "success",
|
|
160
|
+
});
|
|
122
161
|
bus.publish("function.success", {
|
|
123
162
|
workerID: awsWorkerID,
|
|
124
163
|
functionID: worker.functionID,
|
|
125
|
-
requestID:
|
|
164
|
+
requestID: requestID,
|
|
126
165
|
body: req.body,
|
|
127
166
|
});
|
|
128
167
|
// Return pooled worker to pool
|
|
@@ -177,12 +216,20 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
177
216
|
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
178
217
|
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
179
218
|
: pooledWorkerID;
|
|
219
|
+
const requestID = req.params.awsRequestId;
|
|
220
|
+
logEventTrace("WORKER_END", {
|
|
221
|
+
requestID,
|
|
222
|
+
functionID: worker.functionID,
|
|
223
|
+
workerID: pooledWorkerID,
|
|
224
|
+
status: "error",
|
|
225
|
+
errorType: req.body.errorType,
|
|
226
|
+
});
|
|
180
227
|
bus.publish("function.error", {
|
|
181
228
|
workerID: awsWorkerID,
|
|
182
229
|
functionID: worker.functionID,
|
|
183
230
|
errorType: req.body.errorType,
|
|
184
231
|
errorMessage: req.body.errorMessage,
|
|
185
|
-
requestID
|
|
232
|
+
requestID,
|
|
186
233
|
trace: req.body.trace,
|
|
187
234
|
});
|
|
188
235
|
// Return pooled worker to pool
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "path";
|
|
1
|
+
import { createDebugFileLogger } from "./debug-file-logger.js";
|
|
3
2
|
// Configuration
|
|
4
3
|
const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
|
|
5
4
|
export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
|
|
@@ -16,7 +15,21 @@ const metrics = {
|
|
|
16
15
|
avgResponseTime: new Map(),
|
|
17
16
|
responseCount: new Map(),
|
|
18
17
|
};
|
|
19
|
-
|
|
18
|
+
// Create pool logger using the abstract logger
|
|
19
|
+
let poolLogger = null;
|
|
20
|
+
function getPoolLogger() {
|
|
21
|
+
if (!DEBUG_POOL)
|
|
22
|
+
return null;
|
|
23
|
+
if (!poolLogger) {
|
|
24
|
+
poolLogger = createDebugFileLogger({
|
|
25
|
+
filePath: DEBUG_POOL_FILE,
|
|
26
|
+
sessionName: "POOL",
|
|
27
|
+
sessionHeader: `POOL_SIZE=${POOL_SIZE} IDLE_TIMEOUT=${IDLE_TIMEOUT}ms BUILD_CONCURRENCY=${SST_BUILD_CONCURRENCY}`,
|
|
28
|
+
width: 100,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return poolLogger;
|
|
32
|
+
}
|
|
20
33
|
// Function name resolver - set by workers.ts
|
|
21
34
|
let functionNameResolver = (id) => id.slice(0, 25);
|
|
22
35
|
/**
|
|
@@ -26,23 +39,6 @@ let functionNameResolver = (id) => id.slice(0, 25);
|
|
|
26
39
|
export function setFunctionNameResolver(resolver) {
|
|
27
40
|
functionNameResolver = resolver;
|
|
28
41
|
}
|
|
29
|
-
function initLogFile() {
|
|
30
|
-
if (!DEBUG_POOL || logStream)
|
|
31
|
-
return;
|
|
32
|
-
try {
|
|
33
|
-
const logDir = path.dirname(DEBUG_POOL_FILE);
|
|
34
|
-
if (!fs.existsSync(logDir)) {
|
|
35
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
logStream = fs.createWriteStream(DEBUG_POOL_FILE, { flags: "w" });
|
|
38
|
-
logStream.write("\n" + "=".repeat(100) + "\n" +
|
|
39
|
-
"[SESSION START] " + new Date().toISOString() + " | POOL_SIZE=" + POOL_SIZE + " IDLE_TIMEOUT=" + IDLE_TIMEOUT + "ms BUILD_CONCURRENCY=" + SST_BUILD_CONCURRENCY + "\n" +
|
|
40
|
-
"=".repeat(100) + "\n");
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Fall back to no logging if file creation fails
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
42
|
// Extract readable function name from handler path
|
|
47
43
|
function getFunctionName(functionID) {
|
|
48
44
|
return functionNameResolver(functionID);
|
|
@@ -57,30 +53,29 @@ function getBottleneckFlags(functionID) {
|
|
|
57
53
|
const coldStarts = metrics.coldStarts.get(functionID) || 0;
|
|
58
54
|
// SATURATED: All pool slots in use
|
|
59
55
|
if (concurrent >= POOL_SIZE) {
|
|
60
|
-
flags.push("
|
|
56
|
+
flags.push("SATURATED");
|
|
61
57
|
}
|
|
62
58
|
// HIGH_LOAD: >70% pool utilization
|
|
63
59
|
else if (concurrent >= POOL_SIZE * 0.7) {
|
|
64
|
-
flags.push("
|
|
60
|
+
flags.push("HIGH_LOAD");
|
|
65
61
|
}
|
|
66
62
|
// COLD_START: Low hit rate indicates frequent cold starts
|
|
67
63
|
if (total >= 5 && hitRate < 30) {
|
|
68
|
-
flags.push("
|
|
64
|
+
flags.push("LOW_REUSE");
|
|
69
65
|
}
|
|
70
66
|
// BOTTLENECK: High cold start ratio
|
|
71
67
|
if (total >= 3 && coldStarts / total > 0.5) {
|
|
72
|
-
flags.push("
|
|
68
|
+
flags.push("COLD_HEAVY");
|
|
73
69
|
}
|
|
74
|
-
return flags.length > 0 ? " " + flags.join(" ") : "";
|
|
70
|
+
return flags.length > 0 ? " [" + flags.join(" ") + "]" : "";
|
|
75
71
|
}
|
|
76
72
|
/**
|
|
77
73
|
* Pool debug logging helper - writes to file with bottleneck detection
|
|
78
74
|
*/
|
|
79
75
|
export function logPool(action, details = {}) {
|
|
80
|
-
|
|
76
|
+
const logger = getPoolLogger();
|
|
77
|
+
if (!logger)
|
|
81
78
|
return;
|
|
82
|
-
initLogFile();
|
|
83
|
-
const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
84
79
|
const funcName = details.functionID
|
|
85
80
|
? getFunctionName(details.functionID)
|
|
86
81
|
: "";
|
|
@@ -95,20 +90,29 @@ export function logPool(action, details = {}) {
|
|
|
95
90
|
const total = metrics.totalRequests.get(details.functionID) || 0;
|
|
96
91
|
const hits = metrics.poolHits.get(details.functionID) || 0;
|
|
97
92
|
const hitRate = total > 0 ? ((hits / total) * 100).toFixed(0) : "0";
|
|
98
|
-
metricsStr =
|
|
99
|
-
}
|
|
100
|
-
// Format details, replacing functionID with funcName
|
|
101
|
-
const filteredDetails = { ...details };
|
|
102
|
-
delete filteredDetails.functionID;
|
|
103
|
-
const detailStr = Object.entries(filteredDetails)
|
|
104
|
-
.map(([k, v]) => k + "=" + (typeof v === "object" ? JSON.stringify(v) : v))
|
|
105
|
-
.join(" ");
|
|
106
|
-
const actionPadded = action.padEnd(14);
|
|
107
|
-
const funcPadded = funcName.padEnd(25);
|
|
108
|
-
const line = "[" + timestamp + "] " + actionPadded + " " + funcPadded + " " + detailStr + metricsStr + bottleneckFlags + "\n";
|
|
109
|
-
if (logStream) {
|
|
110
|
-
logStream.write(line);
|
|
93
|
+
metricsStr = `concurrent=${concurrent} hitRate=${hitRate}%`;
|
|
111
94
|
}
|
|
95
|
+
// Build the log details
|
|
96
|
+
const logDetails = {};
|
|
97
|
+
// Add function name first if present
|
|
98
|
+
if (funcName) {
|
|
99
|
+
logDetails.func = funcName;
|
|
100
|
+
}
|
|
101
|
+
// Add all other details except functionID
|
|
102
|
+
for (const [k, v] of Object.entries(details)) {
|
|
103
|
+
if (k !== "functionID") {
|
|
104
|
+
logDetails[k] = v;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Add metrics if present
|
|
108
|
+
if (metricsStr) {
|
|
109
|
+
logDetails.metrics = metricsStr;
|
|
110
|
+
}
|
|
111
|
+
// Add bottleneck flags
|
|
112
|
+
if (bottleneckFlags) {
|
|
113
|
+
logDetails.status = bottleneckFlags.trim();
|
|
114
|
+
}
|
|
115
|
+
logger.log(action, logDetails);
|
|
112
116
|
}
|
|
113
117
|
/**
|
|
114
118
|
* Track request lifecycle for metrics - called at request start
|
|
@@ -142,49 +146,40 @@ export function trackRequestEnd(functionID) {
|
|
|
142
146
|
* Called from workers.ts on process exit
|
|
143
147
|
*/
|
|
144
148
|
export function writeSessionEndSummary() {
|
|
145
|
-
|
|
149
|
+
const logger = getPoolLogger();
|
|
150
|
+
if (!logger)
|
|
146
151
|
return;
|
|
147
|
-
let summary = "
|
|
152
|
+
let summary = "";
|
|
148
153
|
for (const [funcID, total] of metrics.totalRequests) {
|
|
149
154
|
const hits = metrics.poolHits.get(funcID) || 0;
|
|
150
155
|
const cold = metrics.coldStarts.get(funcID) || 0;
|
|
151
156
|
const peak = metrics.peakConcurrent.get(funcID) || 0;
|
|
152
157
|
const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : "0";
|
|
153
158
|
const funcName = getFunctionName(funcID);
|
|
154
|
-
summary +=
|
|
159
|
+
summary += ` ${funcName.padEnd(25)} total=${total} poolHits=${hits} coldStarts=${cold} peakConcurrent=${peak} hitRate=${hitRate}%\n`;
|
|
155
160
|
}
|
|
156
|
-
|
|
157
|
-
logStream.write(summary);
|
|
158
|
-
logStream.end();
|
|
161
|
+
logger.close(summary);
|
|
159
162
|
}
|
|
160
|
-
// Invocation trace logging -
|
|
161
|
-
let
|
|
162
|
-
function
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
traceStream = fs.createWriteStream(INVOKE_TRACE_FILE, { flags: "w" });
|
|
171
|
-
traceStream.write("\n" + "=".repeat(80) + "\n" +
|
|
172
|
-
"[TRACE START] " + new Date().toISOString() + "\n" +
|
|
173
|
-
"=".repeat(80) + "\n");
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
// Fall back to no logging if file creation fails
|
|
163
|
+
// Invocation trace logging - uses the abstract logger
|
|
164
|
+
let traceLogger = null;
|
|
165
|
+
function getTraceLogger() {
|
|
166
|
+
if (!traceLogger) {
|
|
167
|
+
traceLogger = createDebugFileLogger({
|
|
168
|
+
filePath: INVOKE_TRACE_FILE,
|
|
169
|
+
sessionName: "TRACE",
|
|
170
|
+
width: 80,
|
|
171
|
+
});
|
|
177
172
|
}
|
|
173
|
+
return traceLogger;
|
|
178
174
|
}
|
|
179
175
|
/**
|
|
180
176
|
* Log invocation trace events to .sst/invoke-trace.log
|
|
181
177
|
* Always enabled for debugging invocation flow
|
|
182
178
|
*/
|
|
183
179
|
export function logInvokeTrace(stage, requestID, details) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
traceStream.write(line);
|
|
180
|
+
const logger = getTraceLogger();
|
|
181
|
+
logger.log(stage, {
|
|
182
|
+
req: requestID.slice(0, 8),
|
|
183
|
+
...(details ? { info: details } : {}),
|
|
184
|
+
});
|
|
190
185
|
}
|
package/runtime/workers.d.ts
CHANGED
|
@@ -23,6 +23,20 @@ declare module "../bus.js" {
|
|
|
23
23
|
functionID: string;
|
|
24
24
|
pooledWorkerID: string;
|
|
25
25
|
};
|
|
26
|
+
"warmup.start": {
|
|
27
|
+
count: number;
|
|
28
|
+
};
|
|
29
|
+
"warmup.progress": {
|
|
30
|
+
completed: number;
|
|
31
|
+
total: number;
|
|
32
|
+
success: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
};
|
|
35
|
+
"warmup.complete": {
|
|
36
|
+
success: number;
|
|
37
|
+
failed: number;
|
|
38
|
+
elapsedMs: number;
|
|
39
|
+
};
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
export declare const useRuntimeWorkers: () => Promise<{
|
|
@@ -40,4 +54,16 @@ export declare const useRuntimeWorkers: () => Promise<{
|
|
|
40
54
|
type: keyof import("../bus.js").Events;
|
|
41
55
|
cb: (payload: any) => void;
|
|
42
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Trigger warmup by invoking Lambda functions with warmup payloads.
|
|
59
|
+
* This sends real requests through the IoT bridge, which naturally creates workers.
|
|
60
|
+
* @param count Number of workers to warm up (default: 15)
|
|
61
|
+
*/
|
|
62
|
+
triggerWarmup(count?: number): Promise<{
|
|
63
|
+
warmed: number;
|
|
64
|
+
elapsed?: undefined;
|
|
65
|
+
} | {
|
|
66
|
+
warmed: number;
|
|
67
|
+
elapsed: number;
|
|
68
|
+
}>;
|
|
43
69
|
}>;
|