Some checks failed
Verify and publish container / build-and-push (push) Failing after 33s
145 lines
5.5 KiB
TypeScript
145 lines
5.5 KiB
TypeScript
import { performance } from "node:perf_hooks";
|
|
import path from "node:path";
|
|
import Database from "better-sqlite3";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
import { prisma } from "@/lib/db";
|
|
import { getActivitySummary } from "@/services/activityService";
|
|
import { listClasses } from "@/services/classService";
|
|
import { getStudyAvailabilityByClass } from "@/services/spacedRepetitionService";
|
|
|
|
const ACTIVITY_ROWS = 100_000;
|
|
const CARD_ROWS = 10_000;
|
|
const SETS = 5;
|
|
const REQUEST_BUDGET_MS = 250;
|
|
const NOW = new Date("2026-08-07T18:00:00.000Z");
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
function databasePath() {
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url?.startsWith("file:./")) {
|
|
throw new Error("Benchmark requires the disposable Vitest database");
|
|
}
|
|
return path.resolve(process.cwd(), url.slice("file:".length));
|
|
}
|
|
|
|
function seedRealApplicationSchema() {
|
|
const database = new Database(databasePath());
|
|
try {
|
|
database.pragma("foreign_keys = ON");
|
|
const insertActivity = database.prepare(
|
|
'INSERT INTO "StudyActivity" ("id", "type", "occurredAt") VALUES (?, ?, ?)'
|
|
);
|
|
const insertDeck = database.prepare(
|
|
'INSERT INTO "Deck" ("id", "classId", "name", "sortOrder") VALUES (?, ?, ?, ?)'
|
|
);
|
|
const insertSet = database.prepare(
|
|
'INSERT INTO "SpacedRepetitionSet" ("id", "classId", "name", "newCardsPerDay", "sortOrder", "updatedAt") VALUES (?, ?, ?, ?, ?, ?)'
|
|
);
|
|
const insertMembership = database.prepare(
|
|
'INSERT INTO "SpacedRepetitionSetDeck" ("setId", "deckId", "sortOrder") VALUES (?, ?, ?)'
|
|
);
|
|
const insertCard = database.prepare(
|
|
'INSERT INTO "Flashcard" ("id", "deckId", "front", "back", "sortOrder") VALUES (?, ?, ?, ?, ?)'
|
|
);
|
|
const insertState = database.prepare(`
|
|
INSERT INTO "SpacedRepetitionCardState"
|
|
("id", "setId", "flashcardId", "due", "stability", "difficulty", "elapsedDays", "scheduledDays", "learningSteps", "reps", "lapses", "state", "firstReviewedAt", "lastReview", "createdAt", "updatedAt")
|
|
VALUES (?, ?, ?, ?, 1, 5, 1, ?, 0, 1, 0, ?, ?, ?, ?, ?)
|
|
`);
|
|
database.transaction(() => {
|
|
database.prepare(
|
|
'INSERT INTO "Class" ("id", "slug", "name", "sortOrder") VALUES (?, ?, ?, 0)'
|
|
).run("benchmark-class", "benchmark-class", "Benchmark Class");
|
|
|
|
for (let index = 0; index < ACTIVITY_ROWS; index += 1) {
|
|
const ageDays = index % 730;
|
|
const type = index % 2 === 0 ? "FLASHCARD" : "QUIZ_QUESTION";
|
|
insertActivity.run(
|
|
`activity-${index}`,
|
|
type,
|
|
NOW.getTime() - ageDays * 86_400_000
|
|
);
|
|
}
|
|
|
|
for (let setIndex = 0; setIndex < SETS; setIndex += 1) {
|
|
const deckId = `benchmark-deck-${setIndex}`;
|
|
const setId = `benchmark-set-${setIndex}`;
|
|
insertDeck.run(deckId, "benchmark-class", `Deck ${setIndex}`, setIndex);
|
|
insertSet.run(setId, "benchmark-class", `Set ${setIndex}`, 30, setIndex, NOW.getTime());
|
|
insertMembership.run(setId, deckId, 0);
|
|
for (let cardIndex = setIndex; cardIndex < CARD_ROWS; cardIndex += SETS) {
|
|
const cardId = `benchmark-card-${cardIndex}`;
|
|
const due = NOW.getTime() + ((cardIndex % 200) - 100) * 60_000;
|
|
const firstReviewed = NOW.getTime() - (cardIndex % 30) * 86_400_000;
|
|
const scheduledDays = cardIndex % 4 === 0 ? 1 : 0;
|
|
const state = cardIndex % 4;
|
|
insertCard.run(cardId, deckId, `Front ${cardIndex}`, `Back ${cardIndex}`, cardIndex);
|
|
insertState.run(
|
|
`benchmark-state-${cardIndex}`,
|
|
setId,
|
|
cardId,
|
|
due,
|
|
scheduledDays,
|
|
state,
|
|
firstReviewed,
|
|
firstReviewed,
|
|
firstReviewed,
|
|
NOW.getTime()
|
|
);
|
|
}
|
|
}
|
|
})();
|
|
} finally {
|
|
database.close();
|
|
}
|
|
}
|
|
|
|
async function measure<T>(label: string, operation: () => Promise<T>) {
|
|
const start = performance.now();
|
|
const value = await operation();
|
|
return {
|
|
label,
|
|
latencyMs: Number((performance.now() - start).toFixed(2)),
|
|
value,
|
|
};
|
|
}
|
|
|
|
describe("remediation performance condition", () => {
|
|
it("measures actual migrated services at the expected upper-use fixture", async () => {
|
|
seedRealApplicationSchema();
|
|
const activity = await measure("getActivitySummary", () => getActivitySummary(NOW));
|
|
const availability = await measure("getStudyAvailabilityByClass", () =>
|
|
getStudyAvailabilityByClass(NOW)
|
|
);
|
|
const classPolling = await measure("listClasses/navbar-poll", () => listClasses());
|
|
const measurements = [activity, availability, classPolling].map((result) => ({
|
|
label: result.label,
|
|
latencyMs: result.latencyMs,
|
|
}));
|
|
|
|
console.info(JSON.stringify({
|
|
fixture: { activityRows: ACTIVITY_ROWS, cards: CARD_ROWS, sets: SETS },
|
|
budgetMs: REQUEST_BUDGET_MS,
|
|
prismaServiceOperations: { activity: 1, availability: 1, classPolling: 2 },
|
|
measurements,
|
|
activity: {
|
|
displayedDays: activity.value.days.length,
|
|
currentStreak: activity.value.currentStreak,
|
|
},
|
|
readyCards: availability.value["benchmark-class"]?.readyCards,
|
|
}, null, 2));
|
|
|
|
expect(activity.value.days).toHaveLength(53 * 7);
|
|
expect(activity.value.currentStreak).toBeGreaterThan(53 * 7);
|
|
expect(availability.value["benchmark-class"]).toBeDefined();
|
|
expect(classPolling.value).toHaveLength(1);
|
|
for (const measurement of measurements) {
|
|
expect(measurement.latencyMs, measurement.label).toBeLessThanOrEqual(
|
|
REQUEST_BUDGET_MS
|
|
);
|
|
}
|
|
});
|
|
});
|