WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 8b17735

Browse files
authored
Merge pull request #5549 from ag-grid/ag-16239/nx-tooling-improvements
AG-16239: Improve Nx batch executor progress reporting and timeout handling
2 parents 4ff7f66 + be15476 commit 8b17735

File tree

4 files changed

+239
-10
lines changed

4 files changed

+239
-10
lines changed

external/ag-shared/scripts/plugin-utils/executors-utils.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import * as os from 'os';
77
import * as path from 'path';
88
import * as ts from 'typescript';
99

10+
import {
11+
createProgressState,
12+
displayProgress,
13+
finishProgress,
14+
setInProgressTasks,
15+
updateProgress,
16+
} from './progress-tracker';
17+
1018
export type TaskResult = {
1119
success: boolean;
1220
terminalOutput: string;
@@ -132,7 +140,11 @@ export function batchExecutor<ExecutorOptions>(
132140
};
133141
}
134142

135-
export function batchWorkerExecutor<ExecutorOptions>(workerModule: string, extraMsgContent?: () => object) {
143+
export function batchWorkerExecutor<ExecutorOptions>(
144+
workerModule: string,
145+
extraMsgContent?: () => object,
146+
timeout?: number // Optional timeout (includes queue time + execution time)
147+
) {
136148
return async function* (
137149
taskGraph: TaskGraph,
138150
inputs: Record<string, ExecutorOptions>,
@@ -161,7 +173,26 @@ export function batchWorkerExecutor<ExecutorOptions>(workerModule: string, extra
161173

162174
const tasks = Object.keys(inputs);
163175

164-
console.info(`Batched execution of ${tasks.length} tasks, using ${pool.threads.length} threads...`);
176+
const timeoutMsg = timeout != null ? ` (${timeout}ms timeout per task)` : '';
177+
console.info(`Batched execution of ${tasks.length} tasks, using ${threadCount} threads${timeoutMsg}...`);
178+
const inProgressTasks = new Set<string>(tasks.slice(0, threadCount));
179+
const progressState = createProgressState(tasks.length);
180+
const progressOptions = {
181+
isTTY: process.stdout.isTTY,
182+
maxInProgressToShow: threadCount,
183+
};
184+
let nextTaskIndex = threadCount;
185+
const finished = (result: BatchExecutorTaskResult) => {
186+
inProgressTasks.delete(result.task);
187+
if (tasks[nextTaskIndex]) {
188+
// FIFO queue, so next sequential task is always the next in the list
189+
inProgressTasks.add(tasks[nextTaskIndex++]);
190+
}
191+
updateProgress(progressState, result.result.success, [...inProgressTasks]);
192+
displayProgress(progressState, progressOptions, false, result.result.success);
193+
return result;
194+
};
195+
165196
const start = performance.now();
166197
const contents = extraMsgContent?.() ?? {};
167198
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
@@ -177,17 +208,31 @@ export function batchWorkerExecutor<ExecutorOptions>(workerModule: string, extra
177208
configurationName: task.target.configuration,
178209
},
179210
taskName,
211+
...(timeout != null ? { timeout } : {}), // Only pass timeout to worker if defined
180212
...contents,
181213
};
182-
results.set(taskName, pool.run(opts));
214+
215+
results.set(
216+
taskName,
217+
pool
218+
.run(opts)
219+
.then((r) => finished(r))
220+
.catch((e) => finished({ task: taskName, result: { success: false, terminalOutput: `${e}` } }))
221+
);
183222
}
184223

185-
// Run yield loop after dispatch to avoid serializing execution.
224+
// Initial progress display
225+
setInProgressTasks(progressState, [...inProgressTasks]);
226+
displayProgress(progressState, progressOptions, true, true);
227+
186228
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
187229
const taskName = tasks[taskIndex];
188230
yield results.get(taskName)!;
189231
}
190232

233+
// Finish progress display
234+
finishProgress(progressOptions);
235+
191236
await Promise.allSettled(results.values());
192237

193238
const duration = performance.now() - start;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
export interface ProgressState {
2+
completed: number;
3+
failed: number;
4+
total: number;
5+
inProgressTasks: string[];
6+
lastLoggedPercentage: number;
7+
}
8+
9+
export interface ProgressTrackerOptions {
10+
isTTY: boolean;
11+
maxInProgressToShow: number;
12+
}
13+
14+
export function shortenTaskName(taskName: string): string {
15+
// Extract meaningful parts from task name
16+
// Common patterns:
17+
// - ag-charts-website-<section>_<page>_<example>_main.ts:generate-example
18+
// - ag-documentation-<project>-<parts>:<target>
19+
20+
// Try to extract last 2-3 meaningful segments before file extension or target
21+
const withoutTarget = taskName.split(':')[0]; // Remove :target suffix
22+
const parts = withoutTarget.split(/[-_]/); // Split on - or _
23+
24+
// Take last 2-3 parts before file extension
25+
const meaningfulParts = parts.filter((p) => p !== 'main' && !p.match(/\.(ts|js)$/)).slice(-3);
26+
27+
const shortened = meaningfulParts.join('_');
28+
29+
// Truncate if still too long
30+
return shortened.length > 30 ? shortened.substring(0, 27) + '...' : shortened;
31+
}
32+
33+
export function createProgressState(total: number): ProgressState {
34+
return {
35+
completed: 0,
36+
failed: 0,
37+
total,
38+
inProgressTasks: [],
39+
lastLoggedPercentage: 0,
40+
};
41+
}
42+
43+
export function updateProgress(state: ProgressState, success: boolean, inProgressTasks: string[]): void {
44+
state.completed++;
45+
if (!success) {
46+
state.failed++;
47+
}
48+
state.inProgressTasks = inProgressTasks;
49+
}
50+
51+
export function setInProgressTasks(state: ProgressState, inProgressTasks: string[]): void {
52+
state.inProgressTasks = inProgressTasks;
53+
}
54+
55+
export function displayProgress(
56+
state: ProgressState,
57+
options: ProgressTrackerOptions,
58+
isFirstTask: boolean,
59+
lastTaskSuccess: boolean
60+
): void {
61+
const percentage = Math.round((state.completed / state.total) * 100);
62+
const failedMsg = state.failed > 0 ? ` - ${state.failed} failed` : '';
63+
const queued = state.total - state.completed;
64+
const progressMsg = `Progress: ${state.completed}/${state.total} (${percentage}%)${failedMsg} | Queued: ${queued}`;
65+
66+
const inProgressMsg =
67+
state.inProgressTasks.length > 0
68+
? `In progress: [${state.inProgressTasks.map(shortenTaskName).slice(0, options.maxInProgressToShow).join(', ')}]`
69+
: '';
70+
71+
if (options.isTTY) {
72+
// Clear previous lines (progress + in-progress)
73+
process.stdout.clearLine(0);
74+
process.stdout.cursorTo(0);
75+
if (!isFirstTask) {
76+
// Move up and clear the previous in-progress line
77+
process.stdout.moveCursor(0, -1);
78+
process.stdout.clearLine(0);
79+
process.stdout.cursorTo(0);
80+
}
81+
82+
// Write progress line
83+
process.stdout.write(progressMsg + '\n');
84+
85+
// Write in-progress tasks line (truncate if needed)
86+
if (inProgressMsg) {
87+
const maxWidth = (process.stdout.columns || 80) - 10;
88+
const truncated =
89+
inProgressMsg.length > maxWidth ? inProgressMsg.substring(0, maxWidth - 3) + '...' : inProgressMsg;
90+
process.stdout.write(truncated);
91+
}
92+
} else {
93+
// Non-TTY: Log every 10% or on failure to avoid spam
94+
const shouldLog =
95+
percentage >= state.lastLoggedPercentage + 10 || !lastTaskSuccess || state.completed === state.total;
96+
97+
if (shouldLog) {
98+
console.info(progressMsg);
99+
if (inProgressMsg) console.info(inProgressMsg);
100+
state.lastLoggedPercentage = percentage;
101+
}
102+
}
103+
}
104+
105+
export function finishProgress(options: ProgressTrackerOptions): void {
106+
if (options.isTTY) {
107+
process.stdout.write('\n');
108+
}
109+
}

plugins/ag-charts-generate-chart-thumbnail/src/executors/generate/batch-instance.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,49 @@ export type Message = {
77
taskName: string;
88
options: ExecutorOptions;
99
context: Pick<ExecutorContext, 'projectName' | 'targetName' | 'configurationName'>;
10+
timeout?: number; // Timeout in milliseconds (optional, defaults to 60000)
1011
};
1112

13+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, taskName: string): Promise<T> {
14+
let timeoutHandle: NodeJS.Timeout;
15+
16+
const timeoutPromise = new Promise<T>((_, reject) => {
17+
timeoutHandle = setTimeout(() => {
18+
if (process.env.DEBUG_TIMEOUT) {
19+
console.log(`[Worker] Timeout fired for ${taskName} after ${timeoutMs}ms`);
20+
}
21+
reject(new Error(`Task '${taskName}' timeout after ${timeoutMs}ms`));
22+
}, timeoutMs);
23+
});
24+
25+
return Promise.race([
26+
promise.finally(() => {
27+
// Clear timeout if promise completes first
28+
if (timeoutHandle) {
29+
clearTimeout(timeoutHandle);
30+
}
31+
}),
32+
timeoutPromise,
33+
]);
34+
}
35+
1236
export default async function processor(msg: Message) {
13-
const { options, context, taskName } = msg;
37+
const { options, context, taskName, timeout = 60000 } = msg;
1438

1539
let result: BatchExecutorTaskResult;
1640
try {
17-
await generateFiles(options, context);
41+
await withTimeout(generateFiles(options, context), timeout, taskName);
1842
result = { task: taskName, result: { success: true, terminalOutput: '' } };
1943
} catch (e) {
20-
result = { task: taskName, result: { success: false, terminalOutput: `${e}` } };
44+
const isTimeout = e instanceof Error && e.message.includes('timeout');
45+
if (isTimeout) {
46+
console.error(`[Worker] Task ${taskName} timed out`);
47+
// Exit the worker process to prevent it from picking up another task
48+
// while the timed-out task is still running
49+
process.exit(1);
50+
}
51+
const terminalOutput = isTimeout ? e.message : `${e}`;
52+
result = { task: taskName, result: { success: false, terminalOutput } };
2153
}
2254

2355
return result;

plugins/ag-charts-generate-example-files/src/executors/generate/batch-instance.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,61 @@ import { type ExecutorOptions, generateFiles } from './executor';
55
export type Message = {
66
taskName: string;
77
options: ExecutorOptions;
8+
timeout?: number; // Timeout in milliseconds (optional, defaults to 60000)
89
};
910

11+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, taskName: string): Promise<T> {
12+
let timeoutHandle: NodeJS.Timeout;
13+
14+
const timeoutPromise = new Promise<T>((_, reject) => {
15+
timeoutHandle = setTimeout(() => {
16+
if (process.env.DEBUG_TIMEOUT) {
17+
console.log(`[Worker] Timeout fired for ${taskName} after ${timeoutMs}ms`);
18+
}
19+
reject(new Error(`Task '${taskName}' timeout after ${timeoutMs}ms`));
20+
}, timeoutMs);
21+
});
22+
23+
return Promise.race([
24+
promise.finally(() => {
25+
// Clear timeout if promise completes first
26+
if (timeoutHandle) {
27+
clearTimeout(timeoutHandle);
28+
}
29+
}),
30+
timeoutPromise,
31+
]);
32+
}
33+
1034
export default async function processor(msg: Message) {
11-
const { options, taskName } = msg;
35+
const { options, taskName, timeout = 60000 } = msg;
36+
37+
// Debug: Log timeout value
38+
if (process.env.DEBUG_TIMEOUT) {
39+
console.log(`[Worker] Task ${taskName} starting with ${timeout}ms timeout`);
40+
}
1241

1342
let result: BatchExecutorTaskResult;
43+
const startTime = Date.now();
1444
try {
15-
await generateFiles(options);
45+
await withTimeout(generateFiles(options), timeout, taskName);
46+
const duration = Date.now() - startTime;
47+
if (process.env.DEBUG_TIMEOUT) {
48+
console.log(`[Worker] Task ${taskName} completed in ${duration}ms`);
49+
}
1650
result = { task: taskName, result: { success: true, terminalOutput: '' } };
1751
} catch (e) {
52+
const duration = Date.now() - startTime;
53+
const isTimeout = e instanceof Error && e.message.includes('timeout');
54+
if (isTimeout) {
55+
console.error(`[Worker] Task ${taskName} timed out after ${duration}ms (limit: ${timeout}ms)`);
56+
// Exit the worker process to prevent it from picking up another task
57+
// while the timed-out task is still running
58+
process.exit(1);
59+
}
1860
console.error(e);
19-
result = { task: taskName, result: { success: false, terminalOutput: `${e.stack}` } };
61+
const terminalOutput = isTimeout ? e.message : `${e.stack}`;
62+
result = { task: taskName, result: { success: false, terminalOutput } };
2063
}
2164

2265
return result;

0 commit comments

Comments
 (0)