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

feat: a simple LRUCache in frontend #20842

Merged
merged 4 commits into from
Jul 26, 2022
Merged

Conversation

zhaoyongjie
Copy link
Member

SUMMARY

the Drill to detail modal needs a LRUCache to handle response data in the client, so making a simple LRUCache in superset-ui/core.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A

TESTING INSTRUCTIONS

passes all CI

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@codecov
Copy link

codecov bot commented Jul 25, 2022

Codecov Report

Merging #20842 (3d78b94) into master (9bf7ed5) will decrease coverage by 0.05%.
The diff coverage is 47.23%.

❗ Current head 3d78b94 differs from pull request most recent head 02fbbdd. Consider uploading reports for the commit 02fbbdd to get more accurate results

@@            Coverage Diff             @@
##           master   #20842      +/-   ##
==========================================
- Coverage   66.33%   66.27%   -0.06%     
==========================================
  Files        1756     1758       +2     
  Lines       66769    66957     +188     
  Branches     7059     7106      +47     
==========================================
+ Hits        44288    44375      +87     
- Misses      20684    20767      +83     
- Partials     1797     1815      +18     
Flag Coverage Δ
javascript 51.93% <47.70%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...ages/superset-ui-core/src/query/types/Dashboard.ts 100.00% <ø> (ø)
...t-frontend/src/dashboard/actions/dashboardState.js 37.05% <ø> (ø)
superset-frontend/src/dashboard/actions/hydrate.js 1.96% <0.00%> (-0.11%) ⬇️
...src/dashboard/components/FiltersBadge/selectors.ts 70.37% <0.00%> (+1.90%) ⬆️
...dashboard/components/SliceHeaderControls/index.tsx 65.88% <0.00%> (+2.35%) ⬆️
...board/components/nativeFilters/FilterBar/index.tsx 60.14% <0.00%> (-0.44%) ⬇️
...nd/src/dashboard/components/nativeFilters/utils.ts 46.66% <ø> (ø)
...perset-frontend/src/dashboard/containers/Chart.jsx 75.00% <ø> (ø)
...set-frontend/src/dashboard/containers/Dashboard.ts 0.00% <ø> (ø)
...shboard/util/charts/getFormDataWithExtraFilters.ts 88.88% <ø> (-5.56%) ⬇️
... and 31 more

Help us with your feedback. Take ten seconds to tell us how you rate us.

expect(() => lruCache(0)).toThrow(Error);
});

test('LRU', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we break this into multiple tests? We could have one to test eviction, one to test invalid key types, and one to test cache clearing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the review. This test case is for entire LRU operations. the L43 depends on the cache instance. The purpose of this test case is to prevent incorrect keys from being inserted at runtime, rather than compile time. the clear() method also depends on the cache instance.

Copy link
Member

Choose a reason for hiding this comment

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

Here's an example:

const newCache = () => {
  const cache = lruCache<string>(3);
  cache.set('1', 'a');
  cache.set('2', 'b');
  cache.set('3', 'c');
  return cache;
};

test('initial LRU', () => {
  expect(lruCache().capacity).toBe(100);
  expect(lruCache(10).capacity).toBe(10);
  expect(lruCache(10).size).toBe(0);
  expect(() => lruCache(0)).toThrow(Error);
});

test('correctly evicts LRU value', () => {
  const cache = newCache();
  expect(cache.size).toBe(3);
  cache.set('4', 'd');
  expect(cache.has('1')).toBeFalsy();
  expect(cache.get('1')).toBeUndefined();
});

test('correctly updates LRU value', () => {
  const cache = newCache();
  cache.get('1');
  cache.set('5', 'e');
  expect(cache.has('1')).toBeTruthy();
  expect(cache.has('2')).toBeFalsy();
});

test('throws exception when key is invalid', () => {
  const cache = lruCache<string>(3);
  // @ts-ignore
  expect(() => cache.set(0)).toThrow(TypeError);
});

test('clears the cache', () => {
  const cache = newCache();
  cache.clear();
  expect(cache.size).toBe(0);
  expect(cache.capacity).toBe(3);
});

This way, when a test fails, it's easier to identify what parts of the code are broken. It also avoids state dependencies between different test cases. You can reuse an initial state or optimize for each test.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is just a test case, do we really need "modularity"? the original test case has 19 lines and the suggestion from you has 32 lines(removed empty line between test case).

image

the newCache is also not a setup fixture, we can't use it in initial LRU and LRU handle null and undefined. This is a completely additional abstraction.

Copy link
Member

Choose a reason for hiding this comment

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

I think readability and separation of concerns are more important than the number of lines. It's not a single test case because they are different requirements. You could have a bug related to value eviction but have the requirements for clearing the cache and type checking the keys still working.

Copy link
Member Author

Choose a reason for hiding this comment

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

a modularity test case and all-in-one test case are not equivalent, because the each get or set method will affect the entire LRU.
For example:

test('LRU operation', () => {
  const cache = lruCache<string>(3);
  cache.set('1', 'a');
  cache.set('2', 'b');
  cache.set('3', 'c');
  cache.set('4', 'd');
  expect(cache.size).toBe(3);
....

not equal to

const newCache = () => {
  const cache = lruCache<string>(3);
  cache.set('1', 'a');
  cache.set('2', 'b');
  cache.set('3', 'c');
  return cache;
};
test('correctly evicts LRU value', () => {
  const cache = newCache();
  expect(cache.size).toBe(3);
  cache.set('4', 'd');
....

Copy link
Member

Choose a reason for hiding this comment

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

If you move the expect after the set then they will be.

Copy link
Member

Choose a reason for hiding this comment

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

The code that I sent you is just an example. Feel free to improve it. For me, the important part is that the each requirement is in its own test case.

Copy link
Member Author

Choose a reason for hiding this comment

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

@michael-s-molina Sorry, my point was that each call to get and set has an effect on the entire LRU, for example, the correctly evicts LRU value and the correctly updates LRU value should operate on a same LRU instance rather than separately. For this argument, we should probably get someone else to take a look at it as well, and I'd be happy to revise it, thank you very much for the review.

Copy link
Member

Choose a reason for hiding this comment

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

I see no problem in merging these two tests together since they are highly related 😉

Copy link
Member

@stephenLYZ stephenLYZ left a comment

Choose a reason for hiding this comment

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

Looks good to me.

Copy link
Member

@ktmud ktmud left a comment

Choose a reason for hiding this comment

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

Given the simplicity of this cache class, I think it's okay to structure basic operations in one test case. However, can we add a test for setting non-string keys? Cache should be expected to return value when trying to retrieve a value for non-string keys, too. (Or throw an error if keys are required to be strings.)

We can always add more tests once there is need to expand the functionalities. Or better yet, just use an established open source solutions (e.g. node-lru-cache) if more requirements arise.

@zhaoyongjie
Copy link
Member Author

@michael-s-molina @ktmud @stephenLYZ Thanks for the review, I will put your consideration into future iterations, if more requirements arise, use the open source solution.

@zhaoyongjie zhaoyongjie merged commit 55a89df into apache:master Jul 26, 2022
@mistercrunch mistercrunch added 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 2.1.0 and removed 🚢 2.1.3 labels Mar 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels size/L 🚢 2.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants