Refactor Study Desk application structure
This commit is contained in:
parent
faaccf8a7e
commit
089439ed90
145 changed files with 8087 additions and 3412 deletions
149
benchmarks/remediationBenchmark.test.ts
Normal file
149
benchmarks/remediationBenchmark.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
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 % 3 === 0
|
||||
? "FLASHCARD"
|
||||
: index % 3 === 1
|
||||
? "QUIZ_QUESTION"
|
||||
: "ARCADE_GROUP";
|
||||
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
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue