Skip to content

Commit

Permalink
perf(core): defer sequence constructor logic
Browse files Browse the repository at this point in the history
  • Loading branch information
marcincichocki committed Oct 19, 2022
1 parent 0e47dde commit 85bb4d1
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 80 deletions.
21 changes: 21 additions & 0 deletions src/common/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Cache } from './cache';

describe('Cache decorator', () => {
it('should cache getter', () => {
const spy = jest.fn();

class Test {
@Cache()
get prop() {
return spy();
}
}

const t = new Test();

t.prop;
t.prop;

expect(spy).toBeCalledTimes(1);
});
});
19 changes: 19 additions & 0 deletions src/common/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Caches getter property after first call. */
export function Cache() {
return (_: unknown, name: PropertyKey, descriptor: PropertyDescriptor) => {
const { get, configurable, enumerable } = descriptor;

descriptor.get = function () {
const value = get.call(this);

Object.defineProperty(this, name, {
configurable,
enumerable,
writable: false,
value,
});

return value;
};
};
}
1 change: 1 addition & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './keyboard';
export * from './util';
export { Cache } from './cache';
9 changes: 4 additions & 5 deletions src/core/solver/breach-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
generateSquareMap,
getUnits,
ROWS,
toHex,
} from '../common';
import { Daemon } from './daemon';
import { HexCodeSequence } from './hex-code-sequence';
import { HierarchyProvider } from './hierarchy/hierarchy-provider';
import { memoizedFindOverlap } from './overlap';
import { Sequence, SequenceJSON } from './sequence';
Expand Down Expand Up @@ -100,10 +100,10 @@ export class BreachProtocolResult implements Serializable {
const pts = this.sequence.parts.map(({ tValue }) => tValue);

return this.game.rawData.daemons
.map((raw, index) => ({ dt: raw.map(fromHex).join(''), index }))
.map((raw, index) => ({ dt: HexCodeSequence.fromHex(raw), index }))
.filter(({ dt }) => !pts.includes(dt))
.filter(({ dt }) => tValue.includes(dt))
.map(({ dt, index }) => new Daemon(dt.split('').map(toHex), index))
.map(({ dt, index }) => new Daemon(dt, index))
.concat(this.sequence.parts);
}

Expand All @@ -113,10 +113,9 @@ export class BreachProtocolResult implements Serializable {
// Produces sequence from resolved path.
private getResolvedSequence() {
const tValue = this.resolvePath(this.path).join('');
const value = tValue.split('').map(toHex);
const parts = this.getResolvedSequenceParts(tValue);

return new Sequence(value, parts);
return new Sequence(tValue, parts);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/solver/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '../common';
import { HexCodeSequence } from './hex-code-sequence';

export class Daemon extends HexCodeSequence<DaemonRawData> {
export class Daemon extends HexCodeSequence {
/** Whether this daemon is a child of another daemon. */
get isChild() {
return this.parents.size > 0;
Expand All @@ -19,7 +19,7 @@ export class Daemon extends HexCodeSequence<DaemonRawData> {
private readonly parents = new Set<Daemon>();
private readonly children = new Set<Daemon>();

constructor(value: DaemonRawData, public readonly index: number) {
constructor(value: DaemonRawData | string, public readonly index: number) {
super(value);
}

Expand Down
32 changes: 27 additions & 5 deletions src/core/solver/hex-code-sequence.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import { fromHex, HexCode } from '../common';
import { Cache } from '@/common';
import { fromHex, HexCode, toHex } from '../common';

export abstract class HexCodeSequence<T extends HexCode[]> {
export abstract class HexCodeSequence {
/** Transformed value each hex code is a one character. */
readonly tValue = this.value.map(fromHex).join('');
@Cache()
get tValue() {
return typeof this.input === 'string'
? this.input
: HexCodeSequence.fromHex(this.input);
}

/** Raw value of in hex. */
@Cache()
get value() {
return typeof this.input === 'string'
? HexCodeSequence.toHex(this.input)
: this.input;
}

/** Length of hex codes. */
readonly length = this.value.length;
readonly length = this.input.length;

constructor(private readonly input: HexCode[] | string) {}

static toHex(value: string) {
return value.split('').map(toHex);
}

constructor(public readonly value: T) {}
static fromHex(value: HexCode[]) {
return value.map(fromHex).join('');
}
}
25 changes: 7 additions & 18 deletions src/core/solver/sequence-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,17 @@ export class SequenceFactory {
return true;
});

permutations: for (const permutation of this.permutate(daemons)) {
let { tValue } = permutation[0];
for (const permutation of this.permutate(daemons)) {
const sequence = Sequence.fromPermutation(
permutation,
this.rawData.bufferSize
);

for (let i = 1; i < permutation.length; i++) {
tValue = memoizedFindOverlap(tValue, permutation[i].tValue);

if (tValue.length > this.rawData.bufferSize) {
continue permutations;
}
}

if (this.registry.has(tValue)) {
if (!sequence || this.registry.has(sequence.tValue)) {
continue;
}

this.registry.add(tValue);

const value = tValue.split('').map(toHex);
const parts = permutation
.flatMap((d) => d.getParts())
.filter(uniqueBy('index'));
const sequence = new Sequence(value, parts);
this.registry.add(sequence.tValue);

if (this.options.immediate) {
yield sequence;
Expand Down
26 changes: 9 additions & 17 deletions src/core/solver/sequence.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ describe('Sequence', () => {
['1C', '1C'],
['55', '7A'],
];
const sequence = new Sequence(
['1C', '1C', '55', '7A'],
Daemon.parse(rawData)
);
const sequence = Sequence.fromPermutation(Daemon.parse(rawData));

expect(sequence.breaks).toEqual([0, 2]);
});
Expand All @@ -22,26 +19,21 @@ describe('Sequence', () => {
['1C', '7A'],
['BD', 'BD'],
];
const sequence = new Sequence(
['1C', '1C', '7A', 'BD', 'BD'],
Daemon.parse(rawData)
);
const sequence = Sequence.fromPermutation(Daemon.parse(rawData));

expect(sequence.breaks).toEqual([0, 3]);
});

it('should have only one break at the start if there is a full overlap', () => {
it('should not create breaks for child daemons and regular daemons', () => {
const rawData: DaemonsRawData = [
['1C', '1C'],
['1C', '1C', '7A'],
['BD', 'BD', 'BD'],
['BD', '1C', 'BD'],
['BD', 'BD'],
];
const daemons = Daemon.parse(rawData);
const sequence = new Sequence(
['1C', '1C', '7A'],
daemons.filter(({ index }) => index === 1)
);
const [d1, d2, d3] = Daemon.parse(rawData);
const sequence = Sequence.fromPermutation([d1, d2]);

expect(daemons[0].isChild).toBe(true);
expect(d3.isChild).toBe(true);
expect(sequence.breaks).toEqual([0]);
});
});
92 changes: 59 additions & 33 deletions src/core/solver/sequence.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,82 @@
import { Serializable } from '@/common';
import { Cache, Serializable, uniqueBy } from '@/common';
import { HexCode } from '../common';
import { Daemon } from './daemon';
import { HexCodeSequence } from './hex-code-sequence';
import { memoizedFindOverlap } from './overlap';

// interface will lose its index signature.
export type SequenceJSON = {
value: HexCode[];
parts: number[];
};

export class Sequence
extends HexCodeSequence<HexCode[]>
implements Serializable
{
readonly indexes = this.parts.map((d) => d.index);

/** List of indexes on which it is safe to break sequence. */
readonly breaks = this.getSequenceBreaks();

constructor(value: HexCode[], public readonly parts: Daemon[]) {
super(value);
export class Sequence extends HexCodeSequence implements Serializable {
@Cache()
get indexes() {
return this.parts.map((d) => d.index);
}

toJSON(): SequenceJSON {
return {
value: this.value,
parts: this.parts.map((p) => p.index),
};
@Cache()
get parts() {
return this.permutation
.flatMap((d) => d.getParts())
.filter(uniqueBy('index'));
}

// Sequence break is an index which doesn't have daemon attached.
// For example daemons: FF 7A and BD BD can create sequence FF 7A BD BD.
// At index 0 and 2 next daemon is not yet started and can be delayed if
// buffer allows it.
// It's not possible to break sequence on overlap. For example daemons:
// FF 7A and 7A BD BD can not be broken on index 2 because they share
// beginning and the end.
private getSequenceBreaks() {
return this.parts
.filter((d) => !d.isChild)
/** List of indexes on which it is safe to break sequence. */
@Cache()
get breaks() {
// Sequence break is an index which doesn't have daemon attached.
// For example daemons: FF 7A and BD BD can create sequence FF 7A BD BD.
// At index 0 and 2 next daemon is not yet started and can be delayed if
// buffer allows it.
// It's not possible to break sequence on overlap. For example daemons:
// FF 7A and 7A BD BD can not be broken on index 2 because they share
// beginning and the end.
return this.permutation
.map((d) => {
const start = this.tValue.indexOf(d.tValue);
const end = start + d.length - 1;

return [start, end];
return { start, end };
})
.filter(([s1], i, array) => {
const prev = array[i - 1];
const e2 = prev ? prev[1] : -1;
.filter(({ start }, i, array) => {
const { end = -1 } = array[i - 1] ?? {};

return s1 > e2;
return start > end;
})
.map(([start]) => start);
.map(({ start }) => start);
}

constructor(
value: HexCode[] | string,
private readonly permutation: Daemon[]
) {
super(value);
}

/** Creates sequence out of permutation of daemons up to given length. */
static fromPermutation(
permutation: Daemon[],
max: number = Number.POSITIVE_INFINITY
): Sequence {
let { tValue } = permutation[0];

for (let i = 1; i < permutation.length; i++) {
tValue = memoizedFindOverlap(tValue, permutation[i].tValue);

if (tValue.length > max) {
return null;
}
}

return new Sequence(tValue, permutation);
}

toJSON(): SequenceJSON {
return {
value: this.value,
parts: this.indexes,
};
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"module": "esnext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"target": "ES2019",
"noImplicitAny": true,
"noImplicitOverride": true,
Expand Down

0 comments on commit 85bb4d1

Please sign in to comment.