diff --git a/benchmarks/kids_scoring_bench.js b/benchmarks/kids_scoring_bench.js new file mode 100644 index 00000000..ffb96a14 --- /dev/null +++ b/benchmarks/kids_scoring_bench.js @@ -0,0 +1,63 @@ +// benchmarks/kids_scoring_bench.js +import { createKidsScorerStage } from "../js/feedEngine/kidsScoring.js"; +import { performance } from "perf_hooks"; + +async function runBenchmark() { + const itemCount = 200000; + const items = []; + + // Create mock items with diverse data to exercise different paths + for (let i = 0; i < itemCount; i++) { + items.push({ + video: { + id: `v${i}`, + pubkey: `pubkey${i % 100}`, + duration: 300 + (i % 600), // 5 to 15 minutes + tags: [ + ["t", "kids"], + ["t", "learning"], + ["t", i % 2 === 0 ? "preschool" : "science"], + ], + viewCount: i * 10, + created_at: Date.now() / 1000 - (i % 10000), + moderation: { trustedCount: i % 5 }, + }, + metadata: {}, + }); + } + + const context = { + runtime: { + ageGroup: "preschool", + trustedAuthors: ["pubkey1", "pubkey2"], + }, + addWhy: () => {}, // Mock addWhy + }; + + const scorer = createKidsScorerStage(); + + console.log(`Starting kids scoring benchmark with ${itemCount} items...`); + + // Warmup + await scorer([...items.slice(0, 1000)], context); + + const iterations = 100; + let totalTime = 0; + + for (let i = 0; i < iterations; i++) { + // Reset metadata for each run to be cleaner + items.forEach(item => item.metadata = {}); + + const start = performance.now(); + await scorer(items, context); + const end = performance.now(); + totalTime += (end - start); + + if (i % 5 === 0) process.stdout.write("."); + } + + const avgTime = totalTime / iterations; + console.log(`\nAverage time per run: ${avgTime.toFixed(4)}ms`); +} + +runBenchmark().catch(console.error); diff --git a/js/feedEngine/kidsScoring.js b/js/feedEngine/kidsScoring.js index 77c9ee7d..ca6af0aa 100644 --- a/js/feedEngine/kidsScoring.js +++ b/js/feedEngine/kidsScoring.js @@ -391,27 +391,32 @@ export function createKidsScorerStage({ const videoId = typeof video.id === "string" ? video.id : null; const pubkey = typeof video.pubkey === "string" ? video.pubkey : null; - const positiveComponents = [ - { key: "age-appropriateness", value: ageAppropriateness }, - { key: "educational-boost", value: educationalBoost }, - { key: "author-trust", value: authorTrust }, - { key: "popularity", value: popularityWithinKids }, - { key: "freshness", value: freshness }, - ]; + let maxKey = "age-appropriateness"; + let maxValue = ageAppropriateness; - let dominantPositive = positiveComponents[0]; - for (const component of positiveComponents) { - if (component.value > dominantPositive.value) { - dominantPositive = component; - } + if (educationalBoost > maxValue) { + maxKey = "educational-boost"; + maxValue = educationalBoost; + } + if (authorTrust > maxValue) { + maxKey = "author-trust"; + maxValue = authorTrust; + } + if (popularityWithinKids > maxValue) { + maxKey = "popularity"; + maxValue = popularityWithinKids; + } + if (freshness > maxValue) { + maxKey = "freshness"; + maxValue = freshness; } - if (dominantPositive.value > 0) { + if (maxValue > 0) { context?.addWhy?.({ stage: stageName, type: "score", - reason: dominantPositive.key, - value: dominantPositive.value, + reason: maxKey, + value: maxValue, score: kidsScore, videoId, pubkey, diff --git a/tests/feed-engine/kids-scorer.test.mjs b/tests/feed-engine/kids-scorer.test.mjs new file mode 100644 index 00000000..b39919e6 --- /dev/null +++ b/tests/feed-engine/kids-scorer.test.mjs @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createKidsScorerStage } from "../../js/feedEngine/kidsScoring.js"; + +test("kids scorer correctly identifies dominant positive component", async () => { + const stage = createKidsScorerStage({ + weights: { + w_age: 0.2, + w_edu: 0.2, + w_author: 0.2, + w_pop: 0.2, + w_fresh: 0.2, + w_risk: 0 + } + }); + + // Create an item where one component is clearly dominant + // Age appropriateness: 1.0 (perfect duration + tags) + // Others: 0 + const items = [ + { + video: { + id: "v1", + pubkey: "pk1", + duration: 300, // 5 min (perfect for toddler/preschool) + tags: [["t", "preschool"], ["t", "learning"]], // Matches preferred + created_at: Date.now() / 1000 - 10000000, // Old + viewCount: 0 + }, + metadata: {}, + }, + ]; + + const why = []; + const context = { + runtime: { + ageGroup: "preschool", + trustedAuthors: [] + }, + addWhy: (detail) => why.push(detail) + }; + + await stage(items, context); + + assert.equal(why.length, 1); + assert.equal(why[0].reason, "age-appropriateness"); + assert.ok(why[0].value > 0); +}); + +test("kids scorer picks educational-boost when dominant", async () => { + const stage = createKidsScorerStage({ + educationalTags: ["math"], + weights: { w_age: 0, w_edu: 1, w_author: 0, w_pop: 0, w_fresh: 0, w_risk: 0 } + }); + + const items = [{ + video: { + id: "v2", + tags: [["t", "math"]], + duration: 300 + }, + metadata: {} + }]; + + const why = []; + await stage(items, { + runtime: { educationalTags: ["math"] }, + addWhy: (d) => why.push(d) + }); + + assert.equal(why.length, 1); + assert.equal(why[0].reason, "educational-boost"); +});