diff --git a/src/core/config.ts b/src/core/config.ts index 27515ecab7..367b5d0255 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -5,18 +5,22 @@ */ import * as ld from "./lodash.ts"; +import { makeTimedFunction } from "./performance/function-times.ts"; -export function mergeConfigs(config: T, ...configs: Array): T { - // copy all configs so we don't mutate them - config = ld.cloneDeep(config); - configs = ld.cloneDeep(configs); +export const mergeConfigs = makeTimedFunction( + "mergeConfigs", + function mergeConfigs(config: T, ...configs: Array): T { + // copy all configs so we don't mutate them + config = ld.cloneDeep(config); + configs = ld.cloneDeep(configs); - return ld.mergeWith( - config, - ...configs, - mergeArrayCustomizer, - ); -} + return ld.mergeWith( + config, + ...configs, + mergeArrayCustomizer, + ); + }, +); export function mergeArrayCustomizer(objValue: unknown, srcValue: unknown) { if (ld.isArray(objValue) || ld.isArray(srcValue)) { diff --git a/src/core/deno-dom.ts b/src/core/deno-dom.ts index 98371ca367..189c4a5dad 100644 --- a/src/core/deno-dom.ts +++ b/src/core/deno-dom.ts @@ -9,20 +9,24 @@ import { debug } from "../deno_ral/log.ts"; import { HTMLDocument, initParser } from "deno_dom/deno-dom-wasm-noinit.ts"; import { register } from "deno_dom/src/parser.ts"; import { DOMParser } from "deno_dom/src/dom/dom-parser.ts"; +import { makeTimedFunctionAsync } from "./performance/function-times.ts"; export async function getDomParser() { await initDenoDom(); return new DOMParser(); } -export async function parseHtml(src: string): Promise { - await initDenoDom(); - const result = (new DOMParser()).parseFromString(src, "text/html"); - if (!result) { - throw new Error("Couldn't parse string into HTML"); - } - return result; -} +export const parseHtml = makeTimedFunctionAsync( + "parseHtml", + async function parseHtml(src: string): Promise { + await initDenoDom(); + const result = (new DOMParser()).parseFromString(src, "text/html"); + if (!result) { + throw new Error("Couldn't parse string into HTML"); + } + return result; + }, +); export async function writeDomToHtmlFile( doc: HTMLDocument, diff --git a/src/core/lodash.ts b/src/core/lodash.ts index 6e88926ad2..7322ec4e96 100644 --- a/src/core/lodash.ts +++ b/src/core/lodash.ts @@ -1,11 +1,10 @@ /* -* lodash.ts -* -* piecemeal exports of lodash to make the tree-shaker happier -* -* Copyright (C) 2022 Posit Software, PBC -* -*/ + * lodash.ts + * + * piecemeal exports of lodash to make the tree-shaker happier + * + * Copyright (C) 2022 Posit Software, PBC + */ import ld_cloneDeep from "lodash/cloneDeep.js"; import ld_debounce from "lodash/debounce.js"; @@ -23,8 +22,9 @@ import ld_isObject from "lodash/isObject.js"; import ld_isEqual from "lodash/isEqual.js"; import ld_orderBy from "lodash/orderBy.js"; import ld_escape from "lodash/escape.js"; +import { makeTimedFunction } from "./performance/function-times.ts"; -export const cloneDeep = ld_cloneDeep; +export const cloneDeep = makeTimedFunction("ld_cloneDeep", ld_cloneDeep); export const debounce = ld_debounce; export const difference = ld_difference; export const each = ld_each; diff --git a/src/core/main.ts b/src/core/main.ts index 73ecfae99f..de3c31b97f 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -12,10 +12,10 @@ import { parse } from "flags"; import { exitWithCleanup } from "./cleanup.ts"; import { captureFileReads, - makeTimedFunctionAsync, type MetricsKeys, reportPerformanceMetrics, } from "./performance/metrics.ts"; +import { makeTimedFunctionAsync } from "./performance/function-times.ts"; import { isWindows } from "../deno_ral/platform.ts"; type Runner = (args: Args) => Promise; @@ -31,7 +31,8 @@ export async function mainRunner(runner: Runner) { Deno.addSignalListener("SIGTERM", abend); } - if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") !== undefined) { + const metricEnv = Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS"); + if (metricEnv === "true" || metricEnv?.split(",").includes("fileReads")) { captureFileReads(); } @@ -47,7 +48,6 @@ export async function mainRunner(runner: Runner) { await new Promise((resolve) => setTimeout(resolve, 10000)); } - const metricEnv = Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS"); if (metricEnv !== undefined) { if (metricEnv !== "true") { reportPerformanceMetrics(metricEnv.split(",") as MetricsKeys[]); diff --git a/src/core/mapped-text.ts b/src/core/mapped-text.ts index 23b7121090..2468dba9af 100644 --- a/src/core/mapped-text.ts +++ b/src/core/mapped-text.ts @@ -15,7 +15,7 @@ import { debug } from "../deno_ral/log.ts"; import * as mt from "./lib/mapped-text.ts"; import { withTiming } from "./timing.ts"; -import { makeTimedFunction } from "./performance/metrics.ts"; +import { makeTimedFunction } from "./performance/function-times.ts"; export type EitherString = mt.EitherString; export type MappedString = mt.MappedString; diff --git a/src/core/performance/function-times.ts b/src/core/performance/function-times.ts new file mode 100644 index 0000000000..c489004622 --- /dev/null +++ b/src/core/performance/function-times.ts @@ -0,0 +1,60 @@ +/* + * function-times.ts + * + * Copyright (C) 2025 Posit Software, PBC + */ + +import { Stats } from "./stats.ts"; + +export const functionTimes: Record = {}; + +// deno-lint-ignore no-explicit-any +export const makeTimedFunction = any>( + name: string, + fn: T, +): T => { + if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) { + return fn; + } + functionTimes[name] = new Stats(); + return function (...args: Parameters): ReturnType { + const start = performance.now(); + try { + const result = fn(...args); + return result; + } finally { + const end = performance.now(); + functionTimes[name].add(end - start); + } + } as T; +}; + +export const timeCall = (callback: () => T): { result: T; time: number } => { + const start = performance.now(); + const result = callback(); + const end = performance.now(); + return { result, time: end - start }; +}; + +export const makeTimedFunctionAsync = < + // deno-lint-ignore no-explicit-any + T extends (...args: any[]) => Promise, +>( + name: string, + fn: T, +): T => { + if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) { + return fn; + } + functionTimes[name] = new Stats(); + return async function (...args: Parameters): Promise> { + const start = performance.now(); + try { + const result = await fn(...args); + return result; + } finally { + const end = performance.now(); + functionTimes[name].add(end - start); + } + } as T; +}; diff --git a/src/core/performance/metrics.ts b/src/core/performance/metrics.ts index 0331abdef0..2a72b2a18d 100644 --- a/src/core/performance/metrics.ts +++ b/src/core/performance/metrics.ts @@ -5,6 +5,7 @@ */ import { inputTargetIndexCacheMetrics } from "../../project/project-index.ts"; +import { functionTimes } from "./function-times.ts"; import { Stats } from "./stats.ts"; type FileReadRecord = { @@ -30,51 +31,6 @@ export function captureFileReads() { }; } -const functionTimes: Record = {}; -// deno-lint-ignore no-explicit-any -export const makeTimedFunction = any>( - name: string, - fn: T, -): T => { - if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) { - return fn; - } - functionTimes[name] = new Stats(); - return function (...args: Parameters): ReturnType { - const start = performance.now(); - try { - const result = fn(...args); - return result; - } finally { - const end = performance.now(); - functionTimes[name].add(end - start); - } - } as T; -}; - -export const makeTimedFunctionAsync = < - // deno-lint-ignore no-explicit-any - T extends (...args: any[]) => Promise, ->( - name: string, - fn: T, -): T => { - if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) { - return fn; - } - functionTimes[name] = new Stats(); - return async function (...args: Parameters): Promise> { - const start = performance.now(); - try { - const result = await fn(...args); - return result; - } finally { - const end = performance.now(); - functionTimes[name].add(end - start); - } - } as T; -}; - const metricsObject = { inputTargetIndexCache: inputTargetIndexCacheMetrics, fileReads, @@ -102,14 +58,14 @@ export function quartoPerformanceMetrics(keys?: MetricsKeys[]) { } export function reportPerformanceMetrics(keys?: MetricsKeys[]) { - console.log("---"); - console.log("Performance metrics"); - console.log("Quarto:"); const content = JSON.stringify(quartoPerformanceMetrics(keys), null, 2); const outFile = Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS_FILE"); if (outFile) { Deno.writeTextFileSync(outFile, content); } else { + console.log("---"); + console.log("Performance metrics"); + console.log("Quarto:"); console.log(content); } } diff --git a/src/core/performance/stats.ts b/src/core/performance/stats.ts index 65f1fe1af3..1f248db841 100644 --- a/src/core/performance/stats.ts +++ b/src/core/performance/stats.ts @@ -37,6 +37,7 @@ export class Stats { if (this.count === 0) { return { count: 0, + total: 0, }; } return { @@ -45,6 +46,7 @@ export class Stats { count: this.count, mean: this.mean, variance: this.m2 / this.count, + total: this.mean * this.count, }; } } diff --git a/src/project/project-context.ts b/src/project/project-context.ts index a1a594e7ca..ed7b4cab27 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -99,6 +99,8 @@ import { computeProjectEnvironment } from "./project-environment.ts"; import { ProjectEnvironment } from "./project-environment-types.ts"; import { NotebookContext } from "../render/notebook/notebook-types.ts"; import { MappedString } from "../core/mapped-text.ts"; +import { timeCall } from "../core/performance/function-times.ts"; +import { assertEquals } from "testing/asserts"; export async function projectContext( path: string, @@ -826,10 +828,9 @@ export async function projectInputFiles( inclusion.engineIntermediates ).flat(); - const inputFiles = ld.difference( - ld.uniq(files), - ld.uniq(intermediateFiles), - ) as string[]; + const inputFiles = Array.from( + new Set(files).difference(new Set(intermediateFiles)), + ); return { files: inputFiles, engines }; }