From 89be58c51752897b29d93f5539efcffdf1b192bc Mon Sep 17 00:00:00 2001 From: gunshiz Date: Thu, 10 Sep 2026 19:08:25 +0700 Subject: [PATCH] perf : better concurrent handle --- README.md | 4 +- components/public/structured-directory.tsx | 1 + k8s/base/deployment.yaml | 4 +- k8s/base/hpa.yaml | 6 +++ lib/guides/queries.ts | 58 ++++++++++++++++------ tests/deployment-contract.test.ts | 4 +- tests/public-performance.test.ts | 1 + 7 files changed, 59 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0e00556..8e523f9 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,8 @@ Kustomize resources live in `k8s/` and define: - ClusterIP port 3000 and Traefik ingress for `guide.sudloh.com`. - Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route. - Startup, liveness, and dependency-aware readiness probes. -- Web requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB. -- HPA from 2 to 6 web replicas at 70% CPU and a PDB with one available. +- Web requests of 500m CPU/1 GiB and limits of 1 CPU/2 GiB. +- HPA from 2 to 6 web replicas at 70% CPU or 75% memory and a PDB with one available. - Non-root, read-only containers with dropped capabilities and seccomp. - ARM64 scheduling constraints matching the production hosts and images. - A versioned migration Job and namespace-scoped CI deployer permissions. diff --git a/components/public/structured-directory.tsx b/components/public/structured-directory.tsx index 9c1b574..777ff4f 100644 --- a/components/public/structured-directory.tsx +++ b/components/public/structured-directory.tsx @@ -62,6 +62,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) { diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml index c0821c0..0f7351b 100644 --- a/k8s/base/deployment.yaml +++ b/k8s/base/deployment.yaml @@ -57,10 +57,10 @@ spec: resources: requests: cpu: 500m - memory: 512Mi + memory: 1Gi limits: cpu: "1" - memory: 1Gi + memory: 2Gi startupProbe: httpGet: path: /api/health diff --git a/k8s/base/hpa.yaml b/k8s/base/hpa.yaml index 04e9f67..882e34a 100644 --- a/k8s/base/hpa.yaml +++ b/k8s/base/hpa.yaml @@ -23,3 +23,9 @@ spec: target: type: Utilization averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 75 diff --git a/lib/guides/queries.ts b/lib/guides/queries.ts index 843a15a..729733a 100644 --- a/lib/guides/queries.ts +++ b/lib/guides/queries.ts @@ -33,6 +33,28 @@ import type { PosterBlendMode } from "@/components/public/material-poster"; const catalogVersionSchema = z.object({ version: z.string() }); const posterBlendModes = new Set(["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]); +// Leave most of the per-pod pool available to other visitors during cache misses. +const PUBLIC_QUERY_CONCURRENCY = 3; + +async function runWithConcurrency< + const T extends ReadonlyArray<() => Promise>, +>( + tasks: T, + concurrency = PUBLIC_QUERY_CONCURRENCY, +): Promise<{ [K in keyof T]: Awaited> }> { + const results = new Array(tasks.length); + let next = 0; + await Promise.all( + Array.from({ length: Math.min(concurrency, tasks.length) }, async () => { + while (next < tasks.length) { + const index = next++; + results[index] = await tasks[index](); + } + }), + ); + return results as { [K in keyof T]: Awaited> }; +} + function catalogCharacterWithUrls( item: typeof catalogCharacters.$inferSelect, materialByKey = new Map(), @@ -221,7 +243,11 @@ export async function listPublicGuideCards() { .from(guides) .innerJoin(media, eq(media.id, guides.coverMediaId)) .where(and(eq(guides.isPublic, true), isNull(guides.trashedAt))) - .orderBy(desc(guides.sortOrder), desc(guides.createdAt), asc(guides.name)), getDb().select().from(glossaryAliases)]); + .orderBy(desc(guides.sortOrder), desc(guides.createdAt), asc(guides.name)), getDb().select({ + kind: glossaryAliases.kind, + catalogKey: glossaryAliases.catalogKey, + alias: glossaryAliases.alias, + }).from(glossaryAliases)]); const grouped = aliasesByItem(aliases); return rows.map(({ coverObjectKey, ...guide }) => ({ ...guide, @@ -303,22 +329,26 @@ async function getGuideContent(slug: string, publicOnly: boolean) { const materialKeys = [...new Set( [...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)), )]; - const [sections, weaponConditionsRows, groups, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await Promise.all([ - getDb().select().from(guideSections).where(and(eq(guideSections.guideId, row.guide.id), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)), - getDb().select().from(weaponConditions).where(eq(weaponConditions.guideId, row.guide.id)).orderBy(asc(weaponConditions.sortOrder)), - getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.sortOrder)), - getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1), - getDb().select().from(constellationConditions).where(eq(constellationConditions.guideId, row.guide.id)).orderBy(asc(constellationConditions.sortOrder)), - getDb().select().from(teamConditions).where(eq(teamConditions.guideId, row.guide.id)).orderBy(asc(teamConditions.sortOrder)), - getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)), - getDb().select().from(glossaryAliases), - materialKeys.length + const [sections, weaponConditionsRows, groups, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await runWithConcurrency([ + () => getDb().select().from(guideSections).where(and(eq(guideSections.guideId, row.guide.id), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)), + () => getDb().select().from(weaponConditions).where(eq(weaponConditions.guideId, row.guide.id)).orderBy(asc(weaponConditions.sortOrder)), + () => getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.sortOrder)), + () => getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1), + () => getDb().select().from(constellationConditions).where(eq(constellationConditions.guideId, row.guide.id)).orderBy(asc(constellationConditions.sortOrder)), + () => getDb().select().from(teamConditions).where(eq(teamConditions.guideId, row.guide.id)).orderBy(asc(teamConditions.sortOrder)), + () => getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)), + () => getDb().select({ + kind: glossaryAliases.kind, + catalogKey: glossaryAliases.catalogKey, + alias: glossaryAliases.alias, + }).from(glossaryAliases), + () => materialKeys.length ? getDb().select().from(catalogMaterials).where(inArray(catalogMaterials.key, materialKeys)) - : [], - row.guide.posterArtMediaId + : Promise.resolve([]), + () => row.guide.posterArtMediaId ? getDb().select().from(media).where(eq(media.id, row.guide.posterArtMediaId)).limit(1) : Promise.resolve([]), - getDb().select({ layer: guidePosterBleeding, asset: media }).from(guidePosterBleeding).leftJoin(media, eq(media.id, guidePosterBleeding.mediaId)).where(eq(guidePosterBleeding.guideId, row.guide.id)).orderBy(asc(guidePosterBleeding.sortOrder)), + () => getDb().select({ layer: guidePosterBleeding, asset: media }).from(guidePosterBleeding).leftJoin(media, eq(media.id, guidePosterBleeding.mediaId)).where(eq(guidePosterBleeding.guideId, row.guide.id)).orderBy(asc(guidePosterBleeding.sortOrder)), ]); const groupedAliases = aliasesByItem(aliases); const materialByKey = new Map(materials.map((item) => [item.key, item])); diff --git a/tests/deployment-contract.test.ts b/tests/deployment-contract.test.ts index 20838cf..75a9610 100644 --- a/tests/deployment-contract.test.ts +++ b/tests/deployment-contract.test.ts @@ -17,7 +17,7 @@ describe("production deployment contract", () => { expect(deployment).toContain("path: /api/health"); expect(deployment).toContain("kubernetes.io/arch: arm64"); expect(deployment).toMatch( - /requests:\s+cpu: 500m\s+memory: 512Mi\s+limits:\s+cpu: "1"\s+memory: 1Gi/u, + /requests:\s+cpu: 500m\s+memory: 1Gi\s+limits:\s+cpu: "1"\s+memory: 2Gi/u, ); expect(deployment).toContain("runAsNonRoot: true"); expect(deployment).toContain("runAsUser: 1000"); @@ -53,6 +53,8 @@ describe("production deployment contract", () => { expect(hpa).toContain("minReplicas: 2"); expect(hpa).toContain("maxReplicas: 6"); expect(hpa).toContain("averageUtilization: 70"); + expect(hpa).toContain("name: memory"); + expect(hpa).toContain("averageUtilization: 75"); expect(pdb).toContain("minAvailable: 1"); }); diff --git a/tests/public-performance.test.ts b/tests/public-performance.test.ts index 83ab490..e2dd90d 100644 --- a/tests/public-performance.test.ts +++ b/tests/public-performance.test.ts @@ -38,6 +38,7 @@ describe("public rendering performance", () => { 'fetchPriority={index === 0 ? "high" : undefined}', ); expect(directory).not.toContain("preload={index === 0}"); + expect(directory).toContain("prefetch={false}"); }); it("publishes a public-only crawler policy", () => {