Skip to content

Commit 2ed6772

Browse files
committed
[feature] perf - add QUARTO_COMBINED_LUA_PROFILE feature
1 parent 4f8336f commit 2ed6772

File tree

7 files changed

+78
-13
lines changed

7 files changed

+78
-13
lines changed

dev-docs/performance-monitoring/pandoc-profiling.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,23 @@ Clone the Pandoc repository, and then
3030
$ C_INCLUDE_PATH=<path-lua-fast-profiler> LIBRARY_PATH=<path-lua-fast-profiler> cabal build pandoc-cli --constraint 'lua +system-lua'
3131
```
3232

33-
## Use the custom Pandoc binary
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.
3437

3538
```
3639
$ QUARTO_PANDOC=<path-to-built-pandoc> quarto render ...
3740
```
3841

39-
To get Lua profiling output from Quarto, use the `lua-profiler-output` metadata option to provide an output file name.
4042
The output will be written as JSON, in a format compatible with [Perfetto](https://ui.perfetto.dev).
4143
The default sampling interval is 5ms, but you can customize that by setting the `lua-profiler-interval-ms` metadata option.
4244

43-
## Analyze profile
45+
### Analyze profile
4446

4547
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export async function filterParamsJson(
130130
filterParams: Record<string, unknown>,
131131
resultsFile: string,
132132
dependenciesFile: string,
133-
) {
133+
): Promise<Record<string, unknown>> {
134134
// extract include params (possibly mutating it's arguments)
135135
const includes = options.format.render[kMergeIncludes] !== false
136136
? extractIncludeParams(
@@ -195,7 +195,7 @@ export async function filterParamsJson(
195195
[kBrand]: options.format.render[kBrand],
196196
"quarto-environment": await quartoEnvironmentParams(options),
197197
};
198-
return JSON.stringify(params);
198+
return params;
199199
}
200200

201201
async function quartoEnvironmentParams(_options: PandocOptions) {

src/command/render/pandoc.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,21 +203,59 @@ import {
203203
} from "../../resources/types/schema-types.ts";
204204
import { kFieldCategories } from "../../project/types/website/listing/website-listing-shared.ts";
205205
import { isWindows } from "../../deno_ral/platform.ts";
206+
import { appendToCombinedLuaProfile } from "../../core/performance/perfetto-utils.ts";
206207

207208
// in case we are running multiple pandoc processes
208209
// we need to make sure we capture all of the trace files
209210
let traceCount = 0;
210211

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+
211240
export async function runPandoc(
212241
options: PandocOptions,
213242
sysFilters: string[],
214243
): Promise<RunPandocResult | null> {
215244
const beforePandocHooks: (() => unknown)[] = [];
216245
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+
217253
const pandocEnv: { [key: string]: string } = {};
218254

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

222260
const traceFilters = pandocMetadata?.["_quarto"]?.["trace-filters"] ||
223261
Deno.env.get("QUARTO_TRACE_FILTERS");
@@ -881,6 +919,14 @@ export async function runPandoc(
881919
dependenciesFile,
882920
);
883921

922+
setupPandocHooks(
923+
handleCombinedLuaProfiles(
924+
options.source,
925+
paramsJson,
926+
options.services.temp,
927+
),
928+
);
929+
884930
// remove selected args and defaults if we are handling some things on behalf of pandoc
885931
// (e.g. handling section numbering). note that section numbering is handled by the
886932
// crossref filter so we only do this if the user hasn't disabled the crossref filter
@@ -1180,13 +1226,13 @@ export async function runPandoc(
11801226
);
11811227
}
11821228

1183-
setupPandocEnv();
1184-
11851229
// run beforePandoc hooks
11861230
for (const hook of beforePandocHooks) {
11871231
await hook();
11881232
}
11891233

1234+
setupPandocEnv();
1235+
11901236
// run pandoc
11911237
const result = await execProcess(
11921238
{

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/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 {

src/resources/filters/ast/runemulation.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ local function emulate_pandoc_filter(filters, afterFilterPass)
131131
return {
132132
traverse = 'topdown',
133133
Pandoc = function(doc)
134-
local profiling = option("lua-profiler-output", false)
134+
local profiling = option("lua-profiler-output", false) or param("lua-profiler-output", false)
135135
if not profiling then
136136
return run_emulated_filter_chain(doc, filters, afterFilterPass), false
137137
end

src/resources/pandoc/datadir/profiler.lua

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ local onDebugHook = function(hookType, line)
2828
while information ~= nil do
2929
local source = information.source or "unknown"
3030
local name = information.name or "anon"
31-
if string.match(source, ".lua$") then
31+
if source:sub(1, 1) == "@" then
3232
outputfile:write(name, " ", source, " ", information.linedefined, "\n")
33+
else
34+
outputfile:write(name, " ", "<inline>", " ", 0, "\n")
3335
end
3436
no = no + 1
3537
information = debug.getinfo(no, "nS")
@@ -45,7 +47,7 @@ end
4547
function module.start(filename, ms)
4648
outputfile = io.open(filename, "a")
4749
if outputfile == nil then
48-
error("Could not open profiler.txt for writing")
50+
error("Could not open " .. filename .. " for writing")
4951
return
5052
end
5153
debug.sethook(onDebugHook, "t", ms or 5) -- NB: "t" debugging only exists in our patched Lua interpreter/pandoc binary!

0 commit comments

Comments
 (0)