1
0

resolver-pool-sizing.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. /**
  2. * Resolver-pool sizing (§7a.1 P1.2): cgroup-honest CPU term + memory-aware
  3. * cap + the CODEGRAPH_RESOLVE_WORKERS override. resolvePoolSize is pure —
  4. * these pin the whole decision matrix, including the two failure modes the
  5. * measurement round exposed: os.cpus() cpuset-blindness (6 workers inside a
  6. * 2-CPU container) and memory-blind sizing (six ~1GB workers OOM-killing a
  7. * 7GB container at true 8-core concurrency).
  8. */
  9. import { describe, it, expect } from 'vitest';
  10. import * as os from 'os';
  11. import { ResolverPool } from '../src/resolution/resolver-pool';
  12. import {
  13. cgroupMemoryAvailable,
  14. darwinMemoryAvailable,
  15. memoryBudgetBytes,
  16. } from '../src/resolution/memory-budget';
  17. const GB = 1024 * 1024 * 1024;
  18. const MB = 1024 * 1024;
  19. function size(opts: Partial<Parameters<typeof ResolverPool.resolvePoolSize>[0]>): number | null {
  20. return ResolverPool.resolvePoolSize({
  21. availableParallelism: 8,
  22. memoryBudget: 16 * GB,
  23. dbSizeBytes: 200 * MB,
  24. ...opts,
  25. });
  26. }
  27. describe('ResolverPool.resolvePoolSize', () => {
  28. it('big dev box: CPU-capped at the long-standing 6', () => {
  29. expect(size({})).toBe(6);
  30. expect(size({ availableParallelism: 11 })).toBe(6);
  31. });
  32. it('true 2-core box gets NO pool — sequential measured faster there (§7a.1: 853s vs 1150s)', () => {
  33. expect(size({ availableParallelism: 2, memoryBudget: 6 * GB })).toBeNull();
  34. expect(size({ availableParallelism: 3, memoryBudget: 6 * GB })).toBe(2);
  35. });
  36. it('kernel-scale DB in a 7GB container: memory term shrinks the pool below the OOM line', () => {
  37. // 4.6GB DB → ~940MB/worker estimate; 5.5GB headroom × 0.7 ≈ 3.85GB → 4 workers.
  38. const s = size({ availableParallelism: 8, memoryBudget: 5.5 * GB, dbSizeBytes: 4.6 * GB });
  39. expect(s).toBe(4);
  40. expect(s!).toBeLessThan(6);
  41. });
  42. it('per-worker estimate is floored (small DBs) and capped (huge DBs)', () => {
  43. // Small DB: floor 256MB/worker — memory cap = 16GB*0.7/256MB = 43 → CPU wins.
  44. expect(size({ dbSizeBytes: 10 * MB })).toBe(6);
  45. // Monster DB: cap 1.5GB/worker — 16GB*0.7/1.5GB = 7 → CPU still wins at 6.
  46. expect(size({ dbSizeBytes: 40 * GB })).toBe(6);
  47. // Same monster DB, tight memory: 4GB*0.7/1.5GB = 1 → below 2 → no pool.
  48. expect(size({ dbSizeBytes: 40 * GB, memoryBudget: 4 * GB })).toBeNull();
  49. });
  50. it('starved memory disables the pool entirely', () => {
  51. expect(size({ memoryBudget: 512 * MB, dbSizeBytes: 4 * GB })).toBeNull();
  52. });
  53. it('CODEGRAPH_RESOLVE_WORKERS overrides everything: 0 disables, values clamp at 16', () => {
  54. expect(size({ explicit: '0' })).toBeNull();
  55. expect(size({ explicit: '3', memoryBudget: 512 * MB })).toBe(3); // override skips the memory term
  56. expect(size({ explicit: '64' })).toBe(16);
  57. expect(size({ explicit: 'nonsense' })).toBe(6); // unparseable → computed path
  58. });
  59. });
  60. describe('memory budget helpers', () => {
  61. it('memoryBudgetBytes is positive and finite on every platform', () => {
  62. const b = memoryBudgetBytes();
  63. expect(b).toBeGreaterThan(0);
  64. expect(Number.isFinite(b)).toBe(true);
  65. });
  66. it('cgroupMemoryAvailable is null when uncontained (non-Linux) and never throws', () => {
  67. const v = cgroupMemoryAvailable();
  68. if (process.platform !== 'linux') {
  69. expect(v).toBeNull();
  70. } else {
  71. // Containerized CI: either uncontained (null) or a sane byte count.
  72. expect(v === null || (v >= 0 && Number.isFinite(v))).toBe(true);
  73. }
  74. });
  75. it.runIf(process.platform === 'darwin')(
  76. 'darwin: available memory counts reclaimable pages, not just free_count',
  77. () => {
  78. const v = darwinMemoryAvailable();
  79. // vm_stat exists on every macOS; a null here means the parse broke.
  80. expect(v).not.toBeNull();
  81. expect(Number.isFinite(v!)).toBe(true);
  82. // The sum includes the free pages freemem() counts, so it can only be
  83. // larger (modulo TOCTOU drift between the two reads — allow slack).
  84. expect(v!).toBeGreaterThanOrEqual(os.freemem() * 0.5);
  85. // And the budget must ride it (the 2-worker strangulation regression:
  86. // a mostly-idle Mac read ~1GB free and halved the resolver pool).
  87. expect(memoryBudgetBytes()).toBeGreaterThanOrEqual(v! * 0.5);
  88. }
  89. );
  90. it.runIf(process.platform !== 'darwin')(
  91. 'darwinMemoryAvailable is null off-macOS and never throws',
  92. () => {
  93. expect(darwinMemoryAvailable()).toBeNull();
  94. }
  95. );
  96. });