Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit c686827

Browse files
committed
Add experimental inaccurate CPU time measurement, closes #161
1 parent 9324178 commit c686827

File tree

10 files changed

+125
-2
lines changed

10 files changed

+125
-2
lines changed

packages/cli-parser/test/help.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Core Options:
6060
handlers
6161
--actual-time Always return correct time from Date [boolean]
6262
methods
63+
--inaccurate-cpu Log inaccurate CPU time measurements [boolean]
6364
6465
Test Options:
6566
-b, --boolean-option Boolean option [boolean]

packages/core/src/plugins/core.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export interface CoreOptions {
133133
globalTimers?: boolean;
134134
globalRandom?: boolean;
135135
actualTime?: boolean;
136+
inaccurateCpu?: boolean;
136137
}
137138

138139
function mapMountEntries(
@@ -400,6 +401,14 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
400401
})
401402
actualTime?: boolean;
402403

404+
@Option({
405+
type: OptionType.BOOLEAN,
406+
description: "Log inaccurate CPU time measurements",
407+
logName: "Inaccurate CPU Time Measurements",
408+
fromWrangler: ({ miniflare }) => miniflare?.inaccurate_cpu,
409+
})
410+
inaccurateCpu?: boolean;
411+
403412
readonly processedModuleRules: ProcessedModuleRule[] = [];
404413

405414
readonly upstreamURL?: URL;
@@ -506,6 +515,13 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
506515
const DecompressionStreamImpl =
507516
webStreams.DecompressionStream ?? DecompressionStream;
508517

518+
if (this.inaccurateCpu) {
519+
ctx.log.warn(
520+
"CPU time measurements are experimental, highly inaccurate and not representative of deployed worker performance.\n" +
521+
"They should only be used for relative comparisons and may be removed in the future."
522+
);
523+
}
524+
509525
// Build globals object
510526
// noinspection JSDeprecatedSymbols
511527
this.#globals = {

packages/core/src/standards/http.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ import {
2121
waitForOpenOutputGate,
2222
} from "@miniflare/shared";
2323
import type { WebSocket } from "@miniflare/web-sockets";
24-
import { Colorize, blue, bold, green, grey, red, yellow } from "kleur/colors";
24+
import {
25+
Colorize,
26+
blue,
27+
bold,
28+
dim,
29+
green,
30+
grey,
31+
red,
32+
yellow,
33+
} from "kleur/colors";
2534
import { splitCookiesString } from "set-cookie-parser";
2635
import {
2736
Request as BaseRequest,
@@ -913,6 +922,10 @@ function millisFromHRTime([seconds, nanoseconds]: HRTime): string {
913922
return `${((seconds * 1e9 + nanoseconds) / 1e6).toFixed(2)}ms`;
914923
}
915924

925+
function millisFromCPUTime(microseconds: number): string {
926+
return `${(microseconds / 1e3).toFixed(2)}ms`;
927+
}
928+
916929
function colourFromHTTPStatus(status: number): Colorize {
917930
if (200 <= status && status < 300) return green;
918931
if (400 <= status && status < 500) return yellow;
@@ -924,19 +937,27 @@ export async function logResponse(
924937
log: Log,
925938
{
926939
start,
940+
startCpu,
927941
method,
928942
url,
929943
status,
930944
waitUntil,
931945
}: {
932946
start: HRTime;
947+
startCpu?: NodeJS.CpuUsage;
933948
method: string;
934949
url: string;
935950
status?: number;
936951
waitUntil?: Promise<any[]>;
937952
}
938953
): Promise<void> {
954+
const cpuParts: string[] = [];
955+
939956
const responseTime = millisFromHRTime(process.hrtime(start));
957+
if (startCpu !== undefined) {
958+
const responseTimeCpu = millisFromCPUTime(process.cpuUsage(startCpu).user);
959+
cpuParts.push(dim(grey(` (CPU: ~${responseTimeCpu}`)));
960+
}
940961

941962
// Wait for all waitUntil promises to resolve
942963
let waitUntilResponse: any[] | undefined;
@@ -948,6 +969,15 @@ export async function logResponse(
948969
log.error(e);
949970
}
950971
const waitUntilTime = millisFromHRTime(process.hrtime(start));
972+
if (startCpu !== undefined) {
973+
if (waitUntilResponse?.length) {
974+
const waitUntilTimeCpu = millisFromCPUTime(
975+
process.cpuUsage(startCpu).user
976+
);
977+
cpuParts.push(dim(grey(`, waitUntil: ~${waitUntilTimeCpu}`)));
978+
}
979+
cpuParts.push(dim(grey(")")));
980+
}
951981

952982
log.log(
953983
[
@@ -961,6 +991,7 @@ export async function logResponse(
961991
// Only include waitUntilTime if there were waitUntil promises
962992
waitUntilResponse?.length ? grey(`, waitUntil: ${waitUntilTime}`) : "",
963993
grey(")"),
994+
...cpuParts,
964995
].join("")
965996
);
966997
}

packages/core/test/plugins/core.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ test("CorePlugin: parses options from wrangler config", async (t) => {
195195
global_timers: true,
196196
global_random: true,
197197
actual_time: true,
198+
inaccurate_cpu: true,
198199
},
199200
},
200201
configDir
@@ -256,6 +257,7 @@ test("CorePlugin: parses options from wrangler config", async (t) => {
256257
globalTimers: true,
257258
globalRandom: true,
258259
actualTime: true,
260+
inaccurateCpu: true,
259261
});
260262
// Check build upload dir defaults to dist
261263
options = parsePluginWranglerConfig(
@@ -301,6 +303,7 @@ test("CorePlugin: logs options", (t) => {
301303
globalTimers: true,
302304
globalRandom: true,
303305
actualTime: true,
306+
inaccurateCpu: true,
304307
});
305308
t.deepEqual(logs, [
306309
// script is OptionType.NONE so omitted
@@ -324,6 +327,7 @@ test("CorePlugin: logs options", (t) => {
324327
"Allow Global Timers: true",
325328
"Allow Global Secure Random: true",
326329
"Actual Time: true",
330+
"Inaccurate CPU Time Measurements: true",
327331
]);
328332
// Check logs default wrangler config/package paths
329333
logs = logPluginOptions(CorePlugin, {

packages/core/test/standards/http.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,3 +1326,33 @@ test("logResponse: logs waitUntil error", async (t) => {
13261326
/GET http:\/\/localhost 200 OK \(\d+.\d{2}ms, waitUntil: \d+.\d{2}ms\)/
13271327
);
13281328
});
1329+
test("logResponse: logs CPU time", async (t) => {
1330+
const log = new TestLog();
1331+
1332+
// Check without waitUntil
1333+
const toLog: Parameters<typeof logResponse>[1] = {
1334+
start: process.hrtime(),
1335+
startCpu: process.cpuUsage(),
1336+
method: "GET",
1337+
url: "http://localhost",
1338+
status: 404,
1339+
};
1340+
await logResponse(log, toLog);
1341+
let [level, message] = log.logs[0];
1342+
t.is(level, LogLevel.NONE);
1343+
t.regex(
1344+
message,
1345+
/GET http:\/\/localhost 404 Not Found \(\d+.\d{2}ms\) \(CPU: ~\d+.\d{2}ms\)/
1346+
);
1347+
1348+
// Check with waitUntil
1349+
log.logs = [];
1350+
toLog.waitUntil = Promise.all([Promise.resolve(42)]);
1351+
await logResponse(log, toLog);
1352+
[level, message] = log.logs[0];
1353+
t.is(level, LogLevel.NONE);
1354+
t.regex(
1355+
message,
1356+
/GET http:\/\/localhost 404 Not Found \(\d+.\d{2}ms, waitUntil: \d+.\d{2}ms\) \(CPU: ~\d+.\d{2}ms, waitUntil: ~\d+.\d{2}ms\)/
1357+
);
1358+
});

packages/http-server/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,9 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
254254
mf: MiniflareCore<Plugins>
255255
): RequestListener {
256256
return async (req, res) => {
257-
const { HTTPPlugin } = await mf.getPlugins();
257+
const { CorePlugin, HTTPPlugin } = await mf.getPlugins();
258258
const start = process.hrtime();
259+
const startCpu = CorePlugin.inaccurateCpu ? process.cpuUsage() : undefined;
259260
const { request, url } = await convertNodeRequest(
260261
req,
261262
await HTTPPlugin.getRequestMeta(req)
@@ -334,6 +335,7 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
334335
assert(req.method && req.url);
335336
await logResponse(mf.log, {
336337
start,
338+
startCpu,
337339
method: req.method,
338340
url: req.url,
339341
status,

packages/http-server/test/index.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,31 @@ test("createRequestListener: should include Content-Length header on responses",
669669
});
670670
});
671671
});
672+
test("createRequestListener: logs response", async (t) => {
673+
const log = new TestLog();
674+
const mf = useMiniflareWithHandler(
675+
{ HTTPPlugin },
676+
{},
677+
(globals) => new globals.Response("body"),
678+
log
679+
);
680+
const port = await listen(t, http.createServer(createRequestListener(mf)));
681+
682+
let [body] = await request(port);
683+
t.is(body, "body");
684+
let logs = log.logsAtLevel(LogLevel.NONE);
685+
t.is(logs.length, 1);
686+
t.regex(logs[0], /^GET \/ 200 OK \(\d+\.\d+ms\)$/);
687+
688+
// Check includes inaccurate CPU time if enabled
689+
await mf.setOptions({ inaccurateCpu: true });
690+
log.logs = [];
691+
[body] = await request(port);
692+
t.is(body, "body");
693+
logs = log.logsAtLevel(LogLevel.NONE);
694+
t.is(logs.length, 1);
695+
t.regex(logs[0], /^GET \/ 200 OK \(\d+\.\d+ms\) \(CPU: ~\d+\.\d+ms\)$/);
696+
});
672697

673698
test("createServer: handles regular requests", async (t) => {
674699
const mf = useMiniflareWithHandler({ HTTPPlugin }, {}, (globals, req) => {

packages/scheduler/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class CronScheduler<Plugins extends SchedulerPluginSignatures> {
2424
// noinspection JSMismatchedCollectionQueryUpdate
2525
private previousValidatedCrons?: Cron[];
2626
private scheduledHandles?: ITimerHandle[];
27+
private inaccurateCpu?: boolean;
2728

2829
constructor(
2930
private readonly mf: MiniflareCore<Plugins>,
@@ -36,6 +37,7 @@ export class CronScheduler<Plugins extends SchedulerPluginSignatures> {
3637

3738
[kReload] = async (event: ReloadEvent<Plugins>): Promise<void> => {
3839
const validatedCrons = event.plugins.SchedulerPlugin.validatedCrons;
40+
this.inaccurateCpu = event.plugins.CorePlugin.inaccurateCpu;
3941
// Checking references here, if different, SchedulerPlugin setup must've
4042
// been called meaning crons changed so reload scheduled tasks
4143
if (this.previousValidatedCrons === validatedCrons) return;
@@ -53,10 +55,12 @@ export class CronScheduler<Plugins extends SchedulerPluginSignatures> {
5355
const spec = cron.toString();
5456
return cronScheduler.setInterval(cron, async () => {
5557
const start = process.hrtime();
58+
const startCpu = this.inaccurateCpu ? process.cpuUsage() : undefined;
5659
// scheduledTime will default to Date.now()
5760
const waitUntil = this.mf.dispatchScheduled(undefined, spec);
5861
await logResponse(this.mf.log, {
5962
start,
63+
startCpu,
6064
method: "SCHD",
6165
url: spec,
6266
waitUntil,

packages/scheduler/test/index.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ test("CronScheduler: schedules tasks for validated CRONs on reload", async (t) =
7272
t.is(events.length, 1);
7373
t.is(events[0].cron, "30 * * * *");
7474
t.regex(log.logs[0][1], /^SCHD 30 \* \* \* \* \(\d+\.\d+ms\)$/);
75+
76+
// Check includes inaccurate CPU time if enabled
77+
await mf.setOptions({ inaccurateCpu: true });
78+
log.logs = [];
79+
await dispatch("30 * * * *");
80+
t.regex(
81+
log.logs[0][1],
82+
/^SCHD 30 \* \* \* \* \(\d+\.\d+ms\) \(CPU: ~\d+\.\d+ms\)$/
83+
);
7584
});
7685
test("CronScheduler: destroys tasks when CRONs change", async (t) => {
7786
const events: ScheduledEvent[] = [];

packages/shared/src/wrangler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface WranglerEnvironmentConfig {
9494
global_timers?: boolean;
9595
global_random?: boolean;
9696
actual_time?: boolean;
97+
inaccurate_cpu?: boolean;
9798
}; // inherited
9899
}
99100

0 commit comments

Comments
 (0)