Skip to content

Commit 62be5a9

Browse files
authored
Merge branch 'main' into jog-as-default
2 parents 0adbab2 + 6d5e2d0 commit 62be5a9

File tree

13 files changed

+217
-153
lines changed

13 files changed

+217
-153
lines changed

dev-docs/checklist-make-a-new-stable-quarto-release copy.md

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
To perform fine-grained profiling of Pandoc Lua filters, we use custom binaries:
2+
3+
- a patch of Lua that supports sample-based profiling
4+
- a custom binary of Pandoc linked against that version of Lua
5+
6+
## Compile the custom Lua version
7+
8+
This has currently only been tested on Macos and Linux.
9+
10+
```
11+
$ git clone [email protected]:cscheid/lua-fast-profiler.git
12+
$ cd lua-fast-profiler
13+
$ git checkout feature/fast-profiler-5.4
14+
$ make
15+
```
16+
17+
You should have `lua` and `liblua.a` in the `lua-fast-profiler` directory.
18+
19+
This version of Lua adds a new debugging hook, "time".
20+
This hook triggers at specified intervals, and is enabled using the letter `t` in the `debug.sethook` call.
21+
The debug hook callback also supports the "alarm" hook, used when calling the `sethook` callback.
22+
23+
Quarto ships with a custom Lua profiler that supports these hooks and performs low-overhead stack profiling.
24+
25+
## Compile the custom Pandoc binary
26+
27+
Clone the Pandoc repository, and then
28+
29+
```
30+
$ C_INCLUDE_PATH=<path-lua-fast-profiler> LIBRARY_PATH=<path-lua-fast-profiler> cabal build pandoc-cli --constraint 'lua +system-lua'
31+
```
32+
33+
## Run Quarto with profiling
34+
35+
To get Lua profiling output from Quarto, use the `lua-profiler-output` metadata option to provide an output file name, and ensure that Quarto uses the
36+
`pandoc` binary you just compiled.
37+
38+
```
39+
$ QUARTO_PANDOC=<path-to-built-pandoc> quarto render ...
40+
```
41+
42+
The output will be written as JSON, in a format compatible with [Perfetto](https://ui.perfetto.dev).
43+
The default sampling interval is 5ms, but you can customize that by setting the `lua-profiler-interval-ms` metadata option.
44+
45+
### Analyze profile
46+
47+
The resulting profile can be visualized and analyzed as a trace file at <https://ui.perfetto.dev>.
48+
49+
### Profiling multiple-file projects
50+
51+
In multiple-file projects, you can get a summary of the runtime of individual filter steps by providing the `QUARTO_COMBINED_LUA_PROFILE` environment variable.
52+
At the end of rendering a project, the name of the file pointed to `QUARTO_COMBINED_LUA_PROFILE` will contain a CSV file with the columns `filter`, `filename`, and `time`, corresponding to an estimate of time (in microseconds) taken by each stage of Quarto's filtering pipeline.

src/command/render/filters.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ const kMediabagDir = "mediabag-dir";
104104

105105
const kResultsFile = "results-file";
106106

107-
const kTimingFile = "timings-file";
108-
109107
const kHasBootstrap = "has-bootstrap";
110108

111109
const kActiveFilters = "active-filters";
@@ -132,8 +130,7 @@ export async function filterParamsJson(
132130
filterParams: Record<string, unknown>,
133131
resultsFile: string,
134132
dependenciesFile: string,
135-
timingFile: string,
136-
) {
133+
): Promise<Record<string, unknown>> {
137134
// extract include params (possibly mutating it's arguments)
138135
const includes = options.format.render[kMergeIncludes] !== false
139136
? extractIncludeParams(
@@ -185,7 +182,6 @@ export async function filterParamsJson(
185182
...customFormatParams,
186183
...typstFilterParams,
187184
[kResultsFile]: pandocMetadataPath(resultsFile),
188-
[kTimingFile]: pandocMetadataPath(timingFile),
189185
[kQuartoFilters]: filterSpec,
190186
[kActiveFilters]: {
191187
normalization: metadataNormalizationFilterActive(options),
@@ -199,7 +195,7 @@ export async function filterParamsJson(
199195
[kBrand]: options.format.render[kBrand],
200196
"quarto-environment": await quartoEnvironmentParams(options),
201197
};
202-
return JSON.stringify(params);
198+
return params;
203199
}
204200

205201
async function quartoEnvironmentParams(_options: PandocOptions) {

src/command/render/pandoc.ts

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,7 @@ import {
181181
processFormatResources,
182182
writeFormatResources,
183183
} from "./pandoc-dependencies-resources.ts";
184-
import {
185-
ExplicitTimingEntry,
186-
getLuaTiming,
187-
insertExplicitTimingEntries,
188-
withTiming,
189-
} from "../../core/timing.ts";
184+
import { withTiming } from "../../core/timing.ts";
190185

191186
import {
192187
requiresShortcodeUnescapePostprocessor,
@@ -208,21 +203,59 @@ import {
208203
} from "../../resources/types/schema-types.ts";
209204
import { kFieldCategories } from "../../project/types/website/listing/website-listing-shared.ts";
210205
import { isWindows } from "../../deno_ral/platform.ts";
206+
import { appendToCombinedLuaProfile } from "../../core/performance/perfetto-utils.ts";
211207

212208
// in case we are running multiple pandoc processes
213209
// we need to make sure we capture all of the trace files
214210
let traceCount = 0;
215211

212+
const handleCombinedLuaProfiles = (
213+
source: string,
214+
paramsJson: Record<string, unknown>,
215+
temp: TempContext,
216+
) => {
217+
const beforePandocHooks: (() => unknown)[] = [];
218+
const afterPandocHooks: (() => unknown)[] = [];
219+
const tmp = temp.createFile();
220+
221+
const combinedProfile = Deno.env.get("QUARTO_COMBINED_LUA_PROFILE");
222+
if (combinedProfile) {
223+
beforePandocHooks.push(() => {
224+
paramsJson["lua-profiler-output"] = tmp;
225+
});
226+
afterPandocHooks.push(() => {
227+
appendToCombinedLuaProfile(
228+
source,
229+
tmp,
230+
combinedProfile,
231+
);
232+
});
233+
}
234+
return {
235+
before: beforePandocHooks,
236+
after: afterPandocHooks,
237+
};
238+
};
239+
216240
export async function runPandoc(
217241
options: PandocOptions,
218242
sysFilters: string[],
219243
): Promise<RunPandocResult | null> {
220244
const beforePandocHooks: (() => unknown)[] = [];
221245
const afterPandocHooks: (() => unknown)[] = [];
246+
const setupPandocHooks = (
247+
hooks: { before: (() => unknown)[]; after: (() => unknown)[] },
248+
) => {
249+
beforePandocHooks.push(...hooks.before);
250+
afterPandocHooks.push(...hooks.after);
251+
};
252+
222253
const pandocEnv: { [key: string]: string } = {};
223254

224255
const setupPandocEnv = () => {
225-
pandocEnv["QUARTO_FILTER_PARAMS"] = encodeBase64(paramsJson);
256+
pandocEnv["QUARTO_FILTER_PARAMS"] = encodeBase64(
257+
JSON.stringify(paramsJson),
258+
);
226259

227260
const traceFilters = pandocMetadata?.["_quarto"]?.["trace-filters"] ||
228261
Deno.env.get("QUARTO_TRACE_FILTERS");
@@ -884,7 +917,14 @@ export async function runPandoc(
884917
formatFilterParams,
885918
filterResultsFile,
886919
dependenciesFile,
887-
timingResultsFile,
920+
);
921+
922+
setupPandocHooks(
923+
handleCombinedLuaProfiles(
924+
options.source,
925+
paramsJson,
926+
options.services.temp,
927+
),
888928
);
889929

890930
// remove selected args and defaults if we are handling some things on behalf of pandoc
@@ -1186,16 +1226,13 @@ export async function runPandoc(
11861226
);
11871227
}
11881228

1189-
// workaround for our wonky Lua timing routines
1190-
const luaEpoch = await getLuaTiming();
1191-
1192-
setupPandocEnv();
1193-
11941229
// run beforePandoc hooks
11951230
for (const hook of beforePandocHooks) {
11961231
await hook();
11971232
}
11981233

1234+
setupPandocEnv();
1235+
11991236
// run pandoc
12001237
const result = await execProcess(
12011238
{
@@ -1231,24 +1268,6 @@ export async function runPandoc(
12311268
}
12321269
}
12331270

1234-
if (existsSync(timingResultsFile)) {
1235-
const timingResultsJSON = Deno.readTextFileSync(timingResultsFile);
1236-
if (
1237-
timingResultsJSON.length > 0 && Deno.env.get("QUARTO_PROFILER_OUTPUT")
1238-
) {
1239-
// workaround for our wonky Lua timing routines
1240-
const luaNow = await getLuaTiming();
1241-
const entries = JSON.parse(timingResultsJSON) as ExplicitTimingEntry[];
1242-
1243-
insertExplicitTimingEntries(
1244-
luaEpoch,
1245-
luaNow,
1246-
entries,
1247-
"pandoc",
1248-
);
1249-
}
1250-
}
1251-
12521271
if (result.success) {
12531272
return {
12541273
inputMetadata: pandocMetadata,

src/core/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "./performance/metrics.ts";
1818
import { makeTimedFunctionAsync } from "./performance/function-times.ts";
1919
import { isWindows } from "../deno_ral/platform.ts";
20+
import { convertCombinedLuaProfileToCSV } from "./performance/perfetto-utils.ts";
2021

2122
type Runner = (args: Args) => Promise<unknown>;
2223
export async function mainRunner(runner: Runner) {
@@ -48,6 +49,11 @@ export async function mainRunner(runner: Runner) {
4849
await new Promise((resolve) => setTimeout(resolve, 10000));
4950
}
5051

52+
const combinedLuaProfile = Deno.env.get("QUARTO_COMBINED_LUA_PROFILE");
53+
if (combinedLuaProfile) {
54+
convertCombinedLuaProfileToCSV(combinedLuaProfile);
55+
}
56+
5157
if (metricEnv !== undefined) {
5258
if (metricEnv !== "true") {
5359
reportPerformanceMetrics(metricEnv.split(",") as MetricsKeys[]);

src/core/performance.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/core/performance/function-times.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export const makeTimedFunction = <T extends (...args: any[]) => any>(
1616
if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) {
1717
return fn;
1818
}
19-
functionTimes[name] = new Stats();
19+
if (!functionTimes[name]) {
20+
functionTimes[name] = new Stats();
21+
}
2022
return function (...args: Parameters<T>): ReturnType<T> {
2123
const start = performance.now();
2224
try {
@@ -46,7 +48,9 @@ export const makeTimedFunctionAsync = <
4648
if (Deno.env.get("QUARTO_REPORT_PERFORMANCE_METRICS") === undefined) {
4749
return fn;
4850
}
49-
functionTimes[name] = new Stats();
51+
if (!functionTimes[name]) {
52+
functionTimes[name] = new Stats();
53+
}
5054
return async function (...args: Parameters<T>): Promise<ReturnType<T>> {
5155
const start = performance.now();
5256
try {

0 commit comments

Comments
 (0)