Skip to content

Commit b421ca4

Browse files
authored
Improve tests (#27)
- Cache real perfs for tests. Perf calculation code is pretty stable now so doesn't make a lot of sense to calculate the super slow real perfs in tests every time. - Speed up convolution. Not noticeable in normal use, but speeds up tests a lot. Performance improved by about 130%. - Fix Deno std version.
1 parent e820e32 commit b421ca4

22 files changed

+311
-193
lines changed

.github/workflows/deno.yml

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Deno CI
1+
name: Tests
22

33
on:
44
push:
@@ -9,13 +9,10 @@ on:
99
jobs:
1010
test:
1111
runs-on: ubuntu-latest
12-
strategy:
13-
matrix:
14-
filter: ['/^(?!perf)/', '/perf.*(1305|1326)/', '/perf.*(1342|1349)/', '/perf.*(1350|1352)/']
1512
steps:
1613
- uses: actions/checkout@v2
1714
- uses: denolib/setup-deno@master
1815
with:
1916
deno-version: v1.x
2017
- name: Run tests
21-
run: deno test --allow-read --filter '${{ matrix.filter }}' carrot/tests/test-*.ts
18+
run: deno test --allow-read carrot/tests/test-*.ts

carrot/src/background/predict.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ for (let i = -RATING_RANGE_LEN; i <= RATING_RANGE_LEN; i++) {
5858

5959
const fftConv = new FFTConv(ELO_WIN_PROB.length + RATING_RANGE_LEN - 1);
6060

61-
class RatingCalculator {
61+
export class RatingCalculator {
6262
constructor(contestants) {
6363
this.contestants = contestants;
6464
this.seed = null;

carrot/src/util/conv.js

+48-51
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
1-
/**
2-
* Represents complex numbers.
3-
*/
4-
class Complex {
5-
constructor(re = 0, im = 0) {
6-
this.re = re;
7-
this.im = im;
8-
}
9-
conj() { return new Complex(this.re, -this.im); }
10-
add(x) { return new Complex(this.re + x.re, this.im + x.im); }
11-
sub(x) { return new Complex(this.re - x.re, this.im - x.im); }
12-
mul(x) { return new Complex(this.re * x.re - this.im * x.im, this.re * x.im + this.im * x.re) }
13-
}
14-
15-
Complex.I = new Complex(0, 1);
16-
171
/**
182
* Performs convolution of real sequences using Cooley-Tukey FFT in O(n log n).
193
*
@@ -30,40 +14,50 @@ export default class FFTConv {
3014
k++;
3115
}
3216
this.n = 1 << k;
33-
this.w = new Array(this.n >> 1);
17+
const n2 = this.n >> 1;
18+
this.wr = [];
19+
this.wi = [];
3420
const ang = 2 * Math.PI / this.n;
35-
for (let i = 0; i < this.w.length; i++) {
36-
this.w[i] = new Complex(Math.cos(i * ang), Math.sin(i * ang));
21+
for (let i = 0; i < n2; i++) {
22+
this.wr[i] = Math.cos(i * ang);
23+
this.wi[i] = Math.sin(i * ang);
3724
}
38-
this.rev = new Array(this.n);
25+
this.rev = [];
3926
this.rev[0] = 0;
4027
for (let i = 1; i < this.n; i++) {
4128
this.rev[i] = (this.rev[i >> 1] >> 1) | ((i & 1) << (k - 1));
4229
}
4330
}
4431

45-
transform(a) {
46-
if (a.length !== this.n) {
47-
throw new Error(`a.length is ${a.length}, expected ${this.n}`);
48-
}
49-
32+
reverse(a) {
5033
for (let i = 1; i < this.n; i++) {
5134
if (i < this.rev[i]) {
5235
const tmp = a[i];
5336
a[i] = a[this.rev[i]];
5437
a[this.rev[i]] = tmp;
5538
}
5639
}
40+
}
41+
42+
transform(ar, ai) {
43+
if (ar.length !== this.n) {
44+
throw new Error(`a.length is ${ar.length}, expected ${this.n}`);
45+
}
5746

47+
this.reverse(ar);
48+
this.reverse(ai);
5849
for (let len = 2; len <= this.n; len <<= 1) {
5950
const half = len >> 1;
6051
const diff = this.n / len;
6152
for (let i = 0; i < this.n; i += len) {
6253
let pw = 0;
6354
for (let j = i; j < i + half; j++) {
64-
const v = a[j + half].mul(this.w[pw]);
65-
a[j + half] = a[j].sub(v);
66-
a[j] = a[j].add(v);
55+
const vr = ar[j + half] * this.wr[pw] - ai[j + half] * this.wi[pw];
56+
const vi = ar[j + half] * this.wi[pw] + ai[j + half] * this.wr[pw];
57+
ar[j + half] = ar[j] - vr;
58+
ai[j + half] = ai[j] - vi;
59+
ar[j] += vr;
60+
ai[j] += vi;
6761
pw += diff;
6862
}
6963
}
@@ -81,32 +75,35 @@ export default class FFTConv {
8175
`a.length + b.length - 1 is ${a.length} + ${b.length} - 1 = ${resLen}, ` +
8276
`expected <= ${n}`);
8377
}
84-
let c = new Array(n);
85-
for (let i = 0; i < n; i++) {
86-
c[i] = new Complex();
87-
}
88-
for (let i = 0; i < a.length; i++) {
89-
c[i].re = a[i];
90-
}
91-
for (let i = 0; i < b.length; i++) {
92-
c[i].im = b[i];
93-
}
94-
this.transform(c);
95-
let res = new Array(n);
96-
let tmpa = c[0].add(c[0].conj());
97-
let tmpb = c[0].conj().sub(c[0]).mul(Complex.I);
98-
res[0] = tmpa.mul(tmpb);
78+
const cr = new Array(n).fill(0);
79+
const ci = new Array(n).fill(0);
80+
cr.splice(0, a.length, ...a);
81+
ci.splice(0, b.length, ...b);
82+
this.transform(cr, ci);
83+
84+
const dr = [];
85+
const di = [];
86+
let tar = 2 * cr[0];
87+
let tai;
88+
let tbr = 2 * ci[0];
89+
let tbi;
90+
dr[0] = tar * tbr;
91+
di[0] = 0;
9992
for (let i = 1; i < n; i++) {
100-
tmpa = c[i].add(c[n - i].conj());
101-
tmpb = c[n - i].conj().sub(c[i]).mul(Complex.I);
102-
res[i] = tmpa.mul(tmpb);
93+
tar = cr[i] + cr[n - i];
94+
tai = ci[i] - ci[n - i];
95+
tbr = ci[n - i] + ci[i];
96+
tbi = cr[n - i] - cr[i];
97+
dr[i] = tar * tbr - tai * tbi;
98+
di[i] = tar * tbi + tai * tbr;
10399
}
104-
this.transform(res);
105-
res[0] = res[0].re / (4 * n);
100+
101+
this.transform(dr, di);
102+
const res = [];
103+
res[0] = dr[0] / (4 * n);
106104
for (let i = 1, j = n - 1; i <= j; i++, j--) {
107-
const tmp = res[i].re;
108-
res[i] = res[j].re / (4 * n);
109-
res[j] = tmp / (4 * n);
105+
res[i] = dr[j] / (4 * n);
106+
res[j] = dr[i] / (4 * n);
110107
}
111108
res.splice(resLen);
112109
return res;

carrot/tests/asserts.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
2-
import { AssertionError } from 'https://deno.land/std/testing/asserts.ts';
3-
import { red, green, white, gray, bold } from 'https://deno.land/std/fmt/colors.ts';
4-
import { diff, DiffType, DiffResult } from 'https://deno.land/std/testing/_diff.ts';
2+
import { AssertionError } from 'https://deno.land/std@0.76.0/testing/asserts.ts';
3+
import { red, green, white, gray, bold } from 'https://deno.land/std@0.76.0/fmt/colors.ts';
4+
import { diff, DiffType, DiffResult } from 'https://deno.land/std@0.76.0/testing/_diff.ts';
55

66
// These are internal functions from https://deno.land/std/testing/asserts.ts.
77
// We use these because we add a new assert function that allows an error margin
@@ -151,4 +151,4 @@ export function assertEqualsWithEps(
151151
throw new AssertionError(message);
152152
}
153153

154-
export * from 'https://deno.land/std/testing/asserts.ts';
154+
export * from 'https://deno.land/std@0.76.0/testing/asserts.ts';

carrot/tests/compare.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as colors from 'https://deno.land/std/fmt/colors.ts';
1+
import * as colors from 'https://deno.land/std@0.76.0/fmt/colors.ts';
22
import * as api from '../src/background/cf-api.js';
33
import predict, { Contestant } from '../src/background/predict.js';
44

carrot/tests/data/round-1305-ozon-2020-div1+2-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/data/round-1326-global-7-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/data/round-1342-edu-86-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/data/round-1349-641-div1-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/data/round-1350-641-div2-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/data/round-1352-640-div4-perfs.json

+1
Large diffs are not rendered by default.

carrot/tests/perf-util.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import predict, { Contestant, PredictResult, MAX_RATING_LIMIT, MIN_RATING_LIMIT } from '../src/background/predict.js';
2+
import binarySearch from '../src/util/binsearch.js';
3+
4+
/** Calculates the delta for a contestant with an assumed before-the-contest rating. */
5+
export function calcDelta(c: Contestant, contestants: Contestant[], assumedRating: number): number {
6+
function replace(handle: string, assumedRating: number) {
7+
return contestants.map(
8+
(c) => c.handle === handle ? new Contestant(handle, c.points, c.penalty, assumedRating) : c);
9+
}
10+
11+
const results: PredictResult[] = predict(replace(c.handle, assumedRating));
12+
return results.filter((r) => r.handle === c.handle)[0].delta;
13+
}
14+
15+
/** Calculates real performance values for the given handles. Very slow. */
16+
export function* calculateRealPerfs(contestants: Contestant[], handles: string[]): Generator<Contestant> {
17+
const handlesSet = new Set(handles);
18+
for (const c of contestants) {
19+
if (!handlesSet.has(c.handle)) {
20+
continue;
21+
}
22+
c.performance =
23+
c.rank === 1 ?
24+
Infinity :
25+
binarySearch(
26+
MIN_RATING_LIMIT, MAX_RATING_LIMIT,
27+
(assumedRating: number) => calcDelta(c, contestants, assumedRating) <= 0);
28+
yield c;
29+
}
30+
}

carrot/tests/perf-worker.ts

+23-30
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,32 @@
1-
import predict, { Contestant, PredictResult, MAX_RATING_LIMIT, MIN_RATING_LIMIT } from '../src/background/predict.js';
2-
import binarySearch from '../src/util/binsearch.js'
1+
import { Contestant } from '../src/background/predict.js';
32

4-
// @ts-ignore: DedicatedWorkerGlobalScope
5-
self.onmessage = (e: MessageEvent): void => {
6-
const contestants: Contestant[] = e.data.contestants;
7-
const fastPerfs: Record<string, number | 'Infinity'> = e.data.fastPerfs;
3+
import { calcDelta, calculateRealPerfs } from './perf-util.ts';
84

9-
function replace(handle: string, assumedRating: number) {
10-
return contestants.map(
11-
(c) => c.handle === handle ? new Contestant(handle, c.points, c.penalty, assumedRating) : c);
5+
// Local function to avoid net dependency
6+
function assert(value: boolean) {
7+
if (!value) {
8+
throw new Error('Assert failed');
129
}
10+
}
1311

14-
function calcDelta(c: Contestant, assumedRating: number): number {
15-
const results: PredictResult[] = predict(replace(c.handle, assumedRating));
16-
return results.filter((r) => r.handle === c.handle)[0].delta;
17-
}
12+
// @ts-ignore: DedicatedWorkerGlobalScope
13+
self.onmessage = (e: MessageEvent): void => {
14+
const contestants: Contestant[] = e.data.contestants;
15+
const handles: string[] = e.data.handles;
1816

19-
for (const c of contestants) {
20-
const fastPerf = fastPerfs[c.handle];
21-
if (fastPerfs[c.handle] === undefined) {
22-
continue;
23-
}
24-
const result: Record<string, any> = {
25-
fastPerf,
26-
deltaAtFastPerf:
27-
fastPerf === 'Infinity' ? c.rank === 1 ? 0 : '-Infinity' : calcDelta(c, fastPerf),
28-
perf:
29-
c.rank === 1 ?
30-
'Infinity' :
31-
binarySearch(
32-
MIN_RATING_LIMIT, MAX_RATING_LIMIT,
33-
(assumedRating: number) => calcDelta(c, assumedRating) <= 0),
17+
for (const c of calculateRealPerfs(contestants, handles)) {
18+
const result = {
19+
handle: c.handle,
20+
perf: c.performance === Infinity ? 'Infinity': c.performance,
3421
};
35-
result.deltaAtPerf = c.rank === 1 ? 0 : calcDelta(c, result.perf),
36-
22+
let deltaAtPerf;
23+
if (c.rank === 1) {
24+
assert(c.performance === Infinity);
25+
deltaAtPerf = 0;
26+
} else {
27+
deltaAtPerf = calcDelta(c, contestants, c.performance);
28+
}
29+
assert(deltaAtPerf === 0);
3730
// @ts-ignore: DedicatedWorkerGlobalScope
3831
self.postMessage(result);
3932
}

0 commit comments

Comments
 (0)