Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Performance] Improve the performance of Radius.tsx by 2x #48

Closed
wants to merge 7 commits into from

Conversation

rortan134
Copy link

Closes #9

I used the moveable function as a benchmark entry point so other results may vary.
Consistently achieved just a bit over a 2x improvement with few to no core changes.

Platform
Windows_NT 10.0.19045 x64
Node.JS: 20.12.2
V8: 11.3.244.8-node.19

fast moveable 0% (base) (16.479 ops/sec) (avg: 60μs)
regular moveable -56,9% (7102 ops/sec) (avg: 140μs)

img

@rortan134
Copy link
Author

All current tests pass but let me know if you want me to add a couple more to make sure

athena/MapData.tsx Outdated Show resolved Hide resolved
athena/Radius.tsx Outdated Show resolved Hide resolved
Comment on lines 68 to 74
const key = vector.toJSON();
let accessible = cacheMap.get(key);
if (accessible != null) {
return accessible;
}
accessible = isAccessibleBase(map, unit, vector);
cacheMap.set(key, accessible);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notes on this:

  • vectors are immutable objects that are all the same, so you can use them directly as keys for maps without stringify-ing them.
  • We cannot cache at the module level a map (MapData instance) changes after mutations, or we might check the movement radius of different maps.

We need one map per MapData instance – and then the question is how we evict the memory since it might be running in a long running process on the client or server.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then the question is how we evict the memory since it might be running in a long running process on the client or server.

First thing that comes to mind is an LRU cache for each map instance in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume we could use a WeakMap?

Copy link
Contributor

@cpojer cpojer Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, a WeakMap<MapData, Map<Vector, boolean>>.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume we could use a WeakMap?

It's viable, although I suggest a more robust library. Let me know what you think about these latest changes

@cpojer
Copy link
Contributor

cpojer commented Jul 8, 2024

That's awesome, thank you for the PR! I left a few comments to make sure we aren't breaking something across maps by caching data globally. Could you also share the benchmark you used?

@rortan134
Copy link
Author

rortan134 commented Jul 8, 2024

Could you also share the benchmark you used?

I used the benchmarkify library with the mock data from the first test in (Radius.test.tsx)

@cpojer
Copy link
Contributor

cpojer commented Jul 8, 2024

Could you also share the benchmark you used?

I used the benchmarkify library with the mock data from the first test in (Radius.test.tsx)

That's great. Can you also share the code so we can repro it?

@rortan134
Copy link
Author

That's great. Can you also share the code so we can repro it?

Absolutely, https://github.com/rortan134/athena-crisis/blob/radius-perf-bmark/athena/__tests__/RadiusBenchmark.test.tsx
Just run the test script

@cpojer
Copy link
Contributor

cpojer commented Jul 9, 2024

Thanks for the updates!

I realized that we can actually avoid all of the extra complexity and don't need to add an LRUCache. The radius cache is specific to a MapData instance, and MapData is immutable. It's just a cache to speed things up, so how about we just add the cache interface to MapData directly? Then it will be garbage collected automatically.

However, this led me to uncover a gap in this optimization. calculateRadius takes four inputs: A map, a unit, a starting Vector and the radius. All four of those can be different at any time, like for example we might want to get a Flamethrower's radius at the center of a desert map, or a Sniper's radius in a volcano map at the left top. However, we might also check different units at one position on the same map, and each unit has different movement costs on the same field, like for example an ocean might be inaccessible to a Flamethrower but a Fighter Jet can just fly over it. This means that caching on a per-map and per-field basis will not work. Let me know if you'd like me to add a bunch of test cases to verify these behaviors.

@rortan134
Copy link
Author

I see.

Let me know if you'd like me to add a bunch of test cases to verify these behaviors.

A couple test cases would be great

Comment on lines +280 to 293
getTileInfo(vector: Vector, layer?: TileLayer, index?: number) {
if (!this.contains(vector)) {
throw new Error(
`getTileInfo: Vector '${vector.x},${vector.y}' is not within the map limits of width '${this.size.width}' and height '${this.size.height}'.`,
);
}

return getTileInfo(this.map[this.getTileIndex(vector)], layer);
return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer);
}

maybeGetTileInfo(vector: Vector, layer?: TileLayer) {
maybeGetTileInfo(vector: Vector, layer?: TileLayer, index?: number) {
if (this.contains(vector)) {
return getTileInfo(this.map[this.getTileIndex(vector)], layer);
return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of changing these functions, let's add getTileInfoByIndex and maybeGetTileInfoByIndex functions.

Comment on lines +213 to 215
getCache() {
return this.unitAccessibilityCache;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getMutableUnitAccessibilityCache

@@ -179,6 +179,7 @@ export default class MapData {
private _hasNeutralUnits: boolean | null = null;
private _activeUnitTypes: ReadonlyMap<PlayerID, ActiveUnitTypes> | null =
null;
protected unitAccessibilityCache: Map<string | number, boolean>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as shared earlier, we can just use Vector directly as keys. vec(1, 1) === vec(1, 1).

@cpojer
Copy link
Contributor

cpojer commented Jul 10, 2024

Thanks for the updates, I think we are getting closer to something that can be merged. As for the benchmark, it's not super representative to run it on the same inputs – if that was a common case we could just cache the outputs of the function completely! I added one new test on main that will fail on this branch.

Here is a more representative benchmark that calculates the radius for each unit on the map:

test('benchmark moveable radius', async () => {
  const testMap = startMap.copy({
    units: startMap.units.delete(vec(11, 8)),
  });
  
  const benchmark = new Benchmark('benchmark moveable').printHeader();
  benchmark
    .createSuite('starting benchmark', { time: 10_000 })
    .add('moveable', () => {
      for (const [vector, unit] of testMap.units) {
        moveable(testMap, unit, vector, 7)
      }
    })
    .add('fast moveable', () => {
      for (const [vector, unit] of testMap.units) {
        fastMoveable(testMap, unit, vector, 7)
      }
    })
  await benchmark.run();
});

It might be worth randomizing the order at the beginning of the run, but this should be a good start.

@cpojer
Copy link
Contributor

cpojer commented Jul 11, 2024

I added the benchmarks right to the repo using pnpm vitest bench: 5d3bc85

Feel free to extend them as well.

@cpojer
Copy link
Contributor

cpojer commented Aug 2, 2024

@rortan134 are you planning on getting back to this?

@cpojer
Copy link
Contributor

cpojer commented Aug 27, 2024

Closing due to inactivity. Happy to reopen if it's being picked up again.

@cpojer cpojer closed this Aug 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Performance] Improve the performance of Radius.tsx by 2x.
2 participants