Merge "Instrument scheduler to investigate slow performance"
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 7de9701..25fb056 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -18,6 +18,8 @@
FetchRequest as FetchRequestBase,
} from '../../../../types/types';
import {ErrorCallback} from '../../../../api/rest';
+import {getAppContext} from '../../../../services/app-context';
+import {Timer} from '../../../../services/gr-reporting/gr-reporting';
import {Scheduler, Task} from '../../../../services/scheduler/scheduler';
import {RetryError} from '../../../../services/scheduler/retry-scheduler';
@@ -261,11 +263,15 @@
private readonly writeScheduler: Scheduler<Response>
) {}
- private schedule(method: string, task: Task<Response>): Promise<Response> {
+ private schedule(
+ method: string,
+ task: Task<Response>,
+ name?: string
+ ): Promise<Response> {
if (method === 'PUT' || method === 'POST' || method === 'DELETE') {
- return this.writeScheduler.schedule(task);
+ return this.writeScheduler.schedule(task, name);
} else {
- return this.readScheduler.schedule(task);
+ return this.readScheduler.schedule(task, name);
}
}
@@ -276,7 +282,41 @@
private fetchImpl(req: FetchRequest): Promise<Response> {
const method = req.fetchOptions?.method ?? HttpMethod.GET;
const startTime = Date.now();
+
+ const isWrite =
+ method === 'PUT' || method === 'POST' || method === 'DELETE';
+ const requestName = `${method} - ${req.anonymizedUrl || 'unknown'}`;
+ const debugRequestName = `${method} - ${req.anonymizedUrl || req.url}`;
+ let schedulerTimer: Timer | undefined;
+ let activeCount = 0;
+
+ if (isWrite) {
+ activeCount = this.writeScheduler.activeCount;
+ console.info(
+ '[SchedulerWait] request:',
+ debugRequestName,
+ 'count:',
+ activeCount,
+ 'active:',
+ this.writeScheduler.activeRequests
+ );
+ // 5 matches the max in flight limit of the write scheduler.
+ if (activeCount >= 5) {
+ schedulerTimer = getAppContext().reportingService.getTimer(
+ 'scheduler-waiting-time'
+ );
+ }
+ }
+
const task = async () => {
+ if (schedulerTimer) {
+ schedulerTimer.end({
+ count: activeCount,
+ request: requestName,
+ activeRequests: this.writeScheduler.activeRequests,
+ });
+ schedulerTimer = undefined;
+ }
const res = await this._auth.fetch(req.url, req.fetchOptions);
// Check for "too many requests" error and throw RetryError to cause a
// retry in this case, if the scheduler attempts retries.
@@ -284,13 +324,15 @@
return res;
};
- const resPromise = this.schedule(method, task).catch((err: unknown) => {
- if (err instanceof RetryError) {
- return err.payload;
- } else {
- throw err;
+ const resPromise = this.schedule(method, task, requestName).catch(
+ (err: unknown) => {
+ if (err instanceof RetryError) {
+ return err.payload;
+ } else {
+ throw err;
+ }
}
- });
+ );
// Log the call after it completes.
resPromise.then(res => this.logCall(req, startTime, res.status));
diff --git a/polygerrit-ui/app/services/scheduler/fake-scheduler.ts b/polygerrit-ui/app/services/scheduler/fake-scheduler.ts
index d4df3ce..c9038c9 100644
--- a/polygerrit-ui/app/services/scheduler/fake-scheduler.ts
+++ b/polygerrit-ui/app/services/scheduler/fake-scheduler.ts
@@ -9,7 +9,15 @@
export class FakeScheduler<T> implements Scheduler<T> {
readonly scheduled: Array<FakeTask> = [];
- schedule(task: Task<T>) {
+ get activeCount() {
+ return this.scheduled.length;
+ }
+
+ get activeRequests() {
+ return [];
+ }
+
+ schedule(task: Task<T>, _name?: string) {
return new Promise<T>((resolve, reject) => {
this.scheduled.push(async (error?: unknown) => {
if (error) {
diff --git a/polygerrit-ui/app/services/scheduler/max-in-flight-scheduler.ts b/polygerrit-ui/app/services/scheduler/max-in-flight-scheduler.ts
index 1febcb6..65e9e13 100644
--- a/polygerrit-ui/app/services/scheduler/max-in-flight-scheduler.ts
+++ b/polygerrit-ui/app/services/scheduler/max-in-flight-scheduler.ts
@@ -8,22 +8,36 @@
export class MaxInFlightScheduler<T> implements Scheduler<T> {
private inflight = 0;
- private waiting: Array<Task<void>> = [];
+ private waiting: Array<{task: Task<void>; name?: string}> = [];
+
+ private readonly running: string[] = [];
+
+ get activeCount(): number {
+ return this.running.length + this.waiting.length;
+ }
+
+ get activeRequests(): string[] {
+ const waitingNames = this.waiting.map(w => w.name || 'unknown');
+ return [...this.running, ...waitingNames];
+ }
constructor(
private readonly base: Scheduler<T>,
private maxInflight: number = 10
) {}
- async schedule(task: Task<T>): Promise<T> {
+ async schedule(task: Task<T>, name?: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
- this.waiting.push(async () => {
- try {
- const result = await this.base.schedule(task);
- resolve(result);
- } catch (e: unknown) {
- reject(e);
- }
+ this.waiting.push({
+ task: async () => {
+ try {
+ const result = await this.base.schedule(task, name);
+ resolve(result);
+ } catch (e: unknown) {
+ reject(e);
+ }
+ },
+ name,
});
this.next();
});
@@ -32,10 +46,14 @@
private next() {
if (this.inflight >= this.maxInflight) return;
if (this.waiting.length === 0) return;
- const task = this.waiting.shift() as Task<void>;
+ const {task, name} = this.waiting.shift()!;
++this.inflight;
+ const taskName = name || 'unknown';
+ this.running.push(taskName);
task().finally(() => {
--this.inflight;
+ const index = this.running.indexOf(taskName);
+ if (index > -1) this.running.splice(index, 1);
this.next();
});
}
diff --git a/polygerrit-ui/app/services/scheduler/retry-scheduler.ts b/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
index d96fc0b..67842a0 100644
--- a/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
+++ b/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
@@ -33,15 +33,26 @@
private backoffFactor: number = 1.618
) {}
- async schedule(task: Task<T>): Promise<T> {
+ get activeCount(): number {
+ return this.base.activeCount;
+ }
+
+ get activeRequests(): string[] {
+ return this.base.activeRequests;
+ }
+
+ async schedule(task: Task<T>, name?: string): Promise<T> {
let tries = 0;
let timeout = this.backoffIntervalMs;
const worker: Task<T> = async () => {
try {
- return await this.base.schedule(task);
+ return await this.base.schedule(task, name);
} catch (e: unknown) {
if (e instanceof RetryError && tries++ < this.maxRetry) {
+ console.info(
+ `[RetryScheduler] Retrying task, try ${tries} after ${timeout}ms`
+ );
await untilTimeout(timeout);
timeout = timeout * this.backoffFactor;
return await worker();
diff --git a/polygerrit-ui/app/services/scheduler/scheduler.ts b/polygerrit-ui/app/services/scheduler/scheduler.ts
index b834ab3..da0c3e4 100644
--- a/polygerrit-ui/app/services/scheduler/scheduler.ts
+++ b/polygerrit-ui/app/services/scheduler/scheduler.ts
@@ -5,10 +5,20 @@
*/
export type Task<T> = () => Promise<T>;
export interface Scheduler<T> {
- schedule(task: Task<T>): Promise<T>;
+ schedule(task: Task<T>, name?: string): Promise<T>;
+ get activeCount(): number;
+ get activeRequests(): string[];
}
export class BaseScheduler<T> implements Scheduler<T> {
- schedule(task: Task<T>) {
+ schedule(task: Task<T>, _name?: string) {
return task();
}
+
+ get activeCount() {
+ return 0;
+ }
+
+ get activeRequests() {
+ return [];
+ }
}