Skip to content

Commit e41dfb8

Browse files
committed
performance - track calling stats when QUARTO_REPORT_PERFORMANCE_METRICS is set
1 parent 8377534 commit e41dfb8

File tree

4 files changed

+145
-12
lines changed

4 files changed

+145
-12
lines changed

src/core/main.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { parse } from "flags";
1212
import { exitWithCleanup } from "./cleanup.ts";
1313
import {
1414
captureFileReads,
15-
reportPeformanceMetrics,
15+
makeTimedFunctionAsync,
16+
type MetricsKeys,
17+
reportPerformanceMetrics,
1618
} from "./performance/metrics.ts";
1719
import { isWindows } from "../deno_ral/platform.ts";
1820

@@ -33,7 +35,10 @@ export async function mainRunner(runner: Runner) {
3335
captureFileReads();
3436
}
3537

36-
await runner(args);
38+
const main = makeTimedFunctionAsync("main", async () => {
39+
return await runner(args);
40+
});
41+
await main();
3742

3843
// if profiling, wait for 10 seconds before quitting
3944
if (Deno.env.get("QUARTO_TS_PROFILE") !== undefined) {
@@ -42,8 +47,13 @@ export async function mainRunner(runner: Runner) {
4247
await new Promise((resolve) => setTimeout(resolve, 10000));
4348
}
4449

45-
if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") !== undefined) {
46-
reportPeformanceMetrics();
50+
const metricEnv = Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS");
51+
if (metricEnv !== undefined) {
52+
if (metricEnv !== "true") {
53+
reportPerformanceMetrics(metricEnv.split(",") as MetricsKeys[]);
54+
} else {
55+
reportPerformanceMetrics();
56+
}
4757
}
4858

4959
exitWithCleanup(0);

src/core/mapped-text.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { debug } from "../deno_ral/log.ts";
1515

1616
import * as mt from "./lib/mapped-text.ts";
1717
import { withTiming } from "./timing.ts";
18+
import { makeTimedFunction } from "./performance/metrics.ts";
1819

1920
export type EitherString = mt.EitherString;
2021
export type MappedString = mt.MappedString;
@@ -33,7 +34,7 @@ export {
3334
// uses a diff algorithm to map on a line-by-line basis target lines
3435
// for `target` to `source`, allowing us to somewhat recover
3536
// MappedString information from third-party tools like knitr.
36-
export function mappedDiff(
37+
function mappedDiffInner(
3738
source: MappedString,
3839
target: string,
3940
) {
@@ -82,6 +83,7 @@ export function mappedDiff(
8283
return mappedString(source, resultChunks, source.fileName);
8384
});
8485
}
86+
export const mappedDiff = makeTimedFunction("mappedDiff", mappedDiffInner);
8587

8688
export function mappedStringFromFile(filename: string): MappedString {
8789
const value = Deno.readTextFileSync(filename);

src/core/performance/metrics.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { inputTargetIndexCacheMetrics } from "../../project/project-index.ts";
8+
import { Stats } from "./stats.ts";
89

910
type FileReadRecord = {
1011
path: string;
@@ -29,16 +30,86 @@ export function captureFileReads() {
2930
};
3031
}
3132

32-
export function quartoPerformanceMetrics() {
33-
return {
34-
inputTargetIndexCache: inputTargetIndexCacheMetrics,
35-
fileReads,
36-
};
33+
const functionTimes: Record<string, Stats> = {};
34+
// deno-lint-ignore no-explicit-any
35+
export const makeTimedFunction = <T extends (...args: any[]) => any>(
36+
name: string,
37+
fn: T,
38+
): T => {
39+
if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) {
40+
return fn;
41+
}
42+
functionTimes[name] = new Stats();
43+
return function (...args: Parameters<T>): ReturnType<T> {
44+
const start = performance.now();
45+
try {
46+
const result = fn(...args);
47+
return result;
48+
} finally {
49+
const end = performance.now();
50+
functionTimes[name].add(end - start);
51+
}
52+
} as T;
53+
};
54+
55+
export const makeTimedFunctionAsync = <
56+
// deno-lint-ignore no-explicit-any
57+
T extends (...args: any[]) => Promise<any>,
58+
>(
59+
name: string,
60+
fn: T,
61+
): T => {
62+
if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) {
63+
return fn;
64+
}
65+
functionTimes[name] = new Stats();
66+
return async function (...args: Parameters<T>): Promise<ReturnType<T>> {
67+
const start = performance.now();
68+
try {
69+
const result = await fn(...args);
70+
return result;
71+
} finally {
72+
const end = performance.now();
73+
functionTimes[name].add(end - start);
74+
}
75+
} as T;
76+
};
77+
78+
const metricsObject = {
79+
inputTargetIndexCache: inputTargetIndexCacheMetrics,
80+
fileReads,
81+
functionTimes,
82+
};
83+
export type MetricsKeys = keyof typeof metricsObject;
84+
85+
export function quartoPerformanceMetrics(keys?: MetricsKeys[]) {
86+
if (!keys) {
87+
return metricsObject;
88+
}
89+
const result: Record<string, unknown> = {};
90+
for (const key of keys) {
91+
if (key === "functionTimes") {
92+
const metricsObjects = metricsObject[key] as Record<string, Stats>;
93+
const entries = Object.entries(metricsObjects);
94+
result[key] = Object.fromEntries(
95+
entries.map(([name, stats]) => [name, stats.report()]),
96+
);
97+
} else {
98+
result[key] = metricsObject[key];
99+
}
100+
}
101+
return result;
37102
}
38103

39-
export function reportPeformanceMetrics() {
104+
export function reportPerformanceMetrics(keys?: MetricsKeys[]) {
40105
console.log("---");
41106
console.log("Performance metrics");
42107
console.log("Quarto:");
43-
console.log(JSON.stringify(quartoPerformanceMetrics(), null, 2));
108+
const content = JSON.stringify(quartoPerformanceMetrics(keys), null, 2);
109+
const outFile = Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS_FILE");
110+
if (outFile) {
111+
Deno.writeTextFileSync(outFile, content);
112+
} else {
113+
console.log(content);
114+
}
44115
}

src/core/performance/stats.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* stats.ts
3+
*
4+
* Capture some sufficient statistics for performance analysis
5+
*
6+
* Copyright (C) 2025 Posit Software, PBC
7+
*/
8+
9+
export class Stats {
10+
// let's use Welford's algorithm for online variance calculation
11+
count: number;
12+
mean: number;
13+
m2: number;
14+
15+
min: number;
16+
max: number;
17+
18+
constructor() {
19+
this.count = 0;
20+
this.mean = 0;
21+
this.m2 = 0;
22+
this.min = Number.MAX_VALUE;
23+
this.max = -Number.MAX_VALUE;
24+
}
25+
26+
add(x: number) {
27+
this.count++;
28+
const delta = x - this.mean;
29+
this.mean += delta / this.count;
30+
const delta2 = x - this.mean;
31+
this.m2 += delta * delta2;
32+
this.min = Math.min(this.min, x);
33+
this.max = Math.max(this.max, x);
34+
}
35+
36+
report() {
37+
if (this.count === 0) {
38+
return {
39+
count: 0,
40+
};
41+
}
42+
return {
43+
min: this.min,
44+
max: this.max,
45+
count: this.count,
46+
mean: this.mean,
47+
variance: this.m2 / this.count,
48+
};
49+
}
50+
}

0 commit comments

Comments
 (0)