blob: d5cf5cf62b96d3993ca7118bdb0d4a774bb8a59b [file]
import { assert } from 'chai';
import { getTraceColor, convertFromDataframe, defaultColors, internals } from './plot-builder';
import { TraceSet, ColumnHeader, Trace } from '../json';
import { MISSING_DATA_SENTINEL } from '../const/const';
import sinon from 'sinon';
describe('plot-builder', () => {
describe('getTraceColor', () => {
it('returns consistent color for same string', () => {
assert.equal(getTraceColor('foo'), getTraceColor('foo'));
});
it('returns different colors for different strings', () => {
assert.notEqual(getTraceColor('foo'), getTraceColor('bar'));
});
it('handles subtest_4=ref correctly', () => {
const baseTrace = ',benchmark=v8,test=JetStream2,';
const refTrace = ',benchmark=v8,subtest_4=ref,test=JetStream2,';
const baseColor = getTraceColor(baseTrace);
const refColor = getTraceColor(refTrace);
// We cannot easily verify exact index without duplicating logic or knowing hash,
// but we can ensure they are distinct if they would have collided, or just distinct in general if no collision.
// Actually, if no collision, they might be distinct by chance.
// The requirement is that *if* collision, we force distinct.
// If we pick a random string, collision is unlikely.
// Let's just ensure they return valid colors.
assert.include(defaultColors, baseColor);
assert.include(defaultColors, refColor);
});
it('ensures ref and pgo do not collide for same base trace', () => {
const refTrace = ',benchmark=v8,subtest_4=ref,test=JetStream2,';
const pgoTrace = ',benchmark=v8,subtest_4=pgo,test=JetStream2,';
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
assert.notEqual(refColor, pgoColor);
});
it('resolves collision when Base and Ref collide, ensuring Pgo also shifts', () => {
// Scenario:
// Base Trace Hash % N = 10
// Ref Trace Hash % N = 10 (Collision with Base)
// Pgo Trace Hash % N = 11 (No initial collision with Base, but would collide with shifted Ref)
// We expect:
// Ref -> Base + 1 = 11
// Pgo -> Base + 2 = 12 (Shifted to avoid collision with Ref)
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_4=ref';
const pgoTrace = 'base_trace,subtest_4=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
// Mock getTraceHash to return specific values that modulo to 10, 10, 11
// defaultColors.length is 20.
// 10 -> 10
// 11 -> 11
stub.withArgs(baseTrace).returns(10);
stub.withArgs(refTrace).returns(10);
stub.withArgs(pgoTrace).returns(11);
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
// Ref should be defaultColors[11]
assert.equal(refColor, defaultColors[11], 'Ref should be shifted to 11');
// Pgo should be defaultColors[12] (not 11!)
assert.equal(pgoColor, defaultColors[12], 'Pgo should be shifted to 12');
} finally {
stub.restore();
}
});
it('resolves 3-way collision (Base, Ref, Pgo all collide)', () => {
// Scenario:
// Base Trace Hash % N = 10
// Ref Trace Hash % N = 10
// Pgo Trace Hash % N = 10
// We expect:
// Ref -> Base + 1 = 11
// Pgo -> Base + 2 = 12
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_4=ref';
const pgoTrace = 'base_trace,subtest_4=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(baseTrace).returns(10);
stub.withArgs(refTrace).returns(10);
stub.withArgs(pgoTrace).returns(10);
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
assert.equal(refColor, defaultColors[11], 'Ref should be shifted to 11');
assert.equal(pgoColor, defaultColors[12], 'Pgo should be shifted to 12');
} finally {
stub.restore();
}
});
it('handles wrap-around when Base is at the end of color array', () => {
// Scenario:
// defaultColors.length = 20
// Base Trace Hash % N = 19
// Ref Trace Hash % N = 19 (Collision with Base)
// Pgo Trace Hash % N = 19 (Collision with Base)
// We expect:
// Ref -> (Base + 1) % 20 = 0
// Pgo -> (Base + 2) % 20 = 1
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_4=ref';
const pgoTrace = 'base_trace,subtest_4=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(baseTrace).returns(19);
stub.withArgs(refTrace).returns(19);
stub.withArgs(pgoTrace).returns(19);
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
// Check against defaultColors[0] and defaultColors[1]
assert.equal(refColor, defaultColors[0], 'Ref should wrap around to 0');
assert.equal(pgoColor, defaultColors[1], 'Pgo should wrap around to 1');
} finally {
stub.restore();
}
});
it('adjusts both Ref and Pgo even if only Pgo collides with Base', () => {
// Scenario:
// Base Trace Hash % N = 10
// Ref Trace Hash % N = 15 (No collision with Base or Pgo)
// Pgo Trace Hash % N = 10 (Collision with Base)
// Since *any* collision in the triplet triggers the offset logic:
// We expect strict offsets:
// Ref -> Base + 1 = 11 (Even though it was 15)
// Pgo -> Base + 2 = 12 (Shifted to avoid Base)
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_4=ref';
const pgoTrace = 'base_trace,subtest_4=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(baseTrace).returns(10);
stub.withArgs(refTrace).returns(15);
stub.withArgs(pgoTrace).returns(10);
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
assert.equal(
refColor,
defaultColors[11],
'Ref should be forced to 11 due to Pgo collision'
);
assert.equal(
pgoColor,
defaultColors[12],
'Pgo should be forced to 12 due to Base collision'
);
} finally {
stub.restore();
}
});
it('handles dynamic subtest indices (subtest_1, subtest_10)', () => {
// Scenario:
// Base Trace Hash % N = 10
// subtest_1=ref Hash % N = 10 (Collision!)
// subtest_1=pgo Hash % N = 11
// Expect Ref -> 11, Pgo -> 12
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_1=ref';
const pgoTrace = 'base_trace,subtest_1=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(baseTrace).returns(10);
stub.withArgs(refTrace).returns(10);
stub.withArgs(pgoTrace).returns(11); // Doesn't matter, will be shifted
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
assert.equal(refColor, defaultColors[11], 'Ref with subtest_1 should be shifted to 11');
assert.equal(pgoColor, defaultColors[12], 'Pgo with subtest_1 should be shifted to 12');
} finally {
stub.restore();
}
});
it('preserves original colors if no collision (Base, Ref, Pgo distinct)', () => {
// Scenario:
// Base Trace Hash % N = 10
// Ref Trace Hash % N = 15
// Pgo Trace Hash % N = 18
// No collisions -> Should keep original colors
const baseTrace = 'base_trace';
const refTrace = 'base_trace,subtest_4=ref';
const pgoTrace = 'base_trace,subtest_4=pgo';
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(baseTrace).returns(10);
stub.withArgs(refTrace).returns(15);
stub.withArgs(pgoTrace).returns(18);
const baseColor = getTraceColor(baseTrace);
const refColor = getTraceColor(refTrace);
const pgoColor = getTraceColor(pgoTrace);
assert.equal(baseColor, defaultColors[10], 'Base should stay at 10');
assert.equal(refColor, defaultColors[15], 'Ref should stay at 15');
assert.equal(pgoColor, defaultColors[18], 'Pgo should stay at 18');
} finally {
stub.restore();
}
});
it('ignores unrelated subtest keys', () => {
// Scenario:
// Trace has subtest_4=something_else
// Should treat it as a normal string and return its original hash
const traceName = 'base_trace,subtest_4=something_else';
const originalHash = 5;
const stub = sinon.stub(internals, 'getTraceHash');
try {
stub.withArgs(traceName).returns(originalHash);
const color = getTraceColor(traceName);
assert.equal(color, defaultColors[5], 'Should return original hash color');
} finally {
stub.restore();
}
});
});
describe('convertFromDataframe', () => {
it('returns null for empty header', () => {
assert.isNull(convertFromDataframe({ traceset: TraceSet({}), header: [] }));
});
it('converts dataframe correctly', () => {
const traceset: TraceSet = TraceSet({
trace1: Trace([1, 2]),
trace2: Trace([3, MISSING_DATA_SENTINEL]),
});
const header: ColumnHeader[] = [
{ offset: 100, timestamp: 1000 },
{ offset: 101, timestamp: 2000 },
] as any;
const result = convertFromDataframe({ traceset, header }, 'commit');
assert.isNotNull(result);
// Row 0: Header
// Row 1: Data point 1
// Row 2: Data point 2
assert.equal(result!.length, 3);
// Header check
// [ {role: domain...}, {type: 'number', label: 'trace1'}, {type: 'number', label: 'trace2'} ]
assert.deepEqual(result![0][1], { type: 'number', label: 'trace1' });
assert.deepEqual(result![0][2], { type: 'number', label: 'trace2' });
// Data check
// Row 1: [100, 1, 3]
assert.equal(result![1][0], 100);
assert.equal(result![1][1], 1);
assert.equal(result![1][2], 3);
// Row 2: [101, 2, null] (missing data sentinel -> null)
assert.equal(result![2][0], 101);
assert.equal(result![2][1], 2);
assert.isNull(result![2][2]);
});
it('converts placeholder dataframe with all missing data correctly', () => {
// Verify that a dataframe containing only missing data points (returned when
// a search matches traces but finds no data in the requested range)
// is handled correctly.
const traceset: TraceSet = TraceSet({
',arch=x86,': Trace([MISSING_DATA_SENTINEL, MISSING_DATA_SENTINEL]),
});
const header: ColumnHeader[] = [
{ offset: 100, timestamp: 1000 },
{ offset: 500, timestamp: 5000 },
] as any;
const result = convertFromDataframe({ traceset, header }, 'commit');
assert.isNotNull(result);
assert.equal(result!.length, 3); // Header + 2 data rows
// Header should be typed correctly
assert.deepEqual(result![0][1], { type: 'number', label: ',arch=x86,' });
// First Data Row: [100, null]
assert.equal(result![1][0], 100);
assert.isNull(result![1][1]);
// Second Data Row: [500, null]
assert.equal(result![2][0], 500);
assert.isNull(result![2][1]);
});
});
});