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

💡 [major] Prefer MSGPack over JSON #93

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

christianblais
Copy link
Contributor

@christianblais christianblais commented Aug 15, 2024

Cachette cached values aren't meant to be human readable. This lib is meant to be fast. Therefore, I believe we can achieve a little bit more speed by avoiding JSON serialization, and instead use more performant algorithms, such as msgpack. With this PR, I'm replacing JSON with msgpacker. We would incur a small hit on writes, but gain interesting wins on reads.

5000 writes;

Platform info:
==============
   Darwin 23.3.0 arm64
   Node.JS: 18.20.4
   V8: 10.2.154.26-node.37
   CPU: Apple M2 × 8
   Memory: 16 GB

Suite: serialize
================

✔ msgpack       126,894 ops/sec
✔ json          131,671 ops/sec

   msgpack        -3.63%    (126,894 ops/sec)   (avg: 7μs)
   json (#)           0%    (131,671 ops/sec)   (avg: 7μs)

┌─────────┬────────────────────────────────────────────────────┐
│ msgpack │ ████████████████████████████████████████████████   │
├─────────┼────────────────────────────────────────────────────┤
│ json    │ ██████████████████████████████████████████████████ │
└─────────┴────────────────────────────────────────────────────┘

5000 reads;

Platform info:
==============
   Darwin 23.3.0 arm64
   Node.JS: 18.20.4
   V8: 10.2.154.26-node.37
   CPU: Apple M2 × 8
   Memory: 16 GB

Suite: parse
================

✔ msgpack        82,745 ops/sec
✔ json           59,111 ops/sec

   msgpack       +39.98%     (82,745 ops/sec)   (avg: 12μs)
   json (#)           0%     (59,111 ops/sec)   (avg: 16μs)

┌─────────┬────────────────────────────────────────────────────┐
│ msgpack │ ██████████████████████████████████████████████████ │
├─────────┼────────────────────────────────────────────────────┤
│ json    │ ████████████████████████████████████               │
└─────────┴────────────────────────────────────────────────────┘

Given cachette is all about optimizing reads, I believe this to be a good trade-off. Here, assuming a 5:1 read:write ratio, we see a 34% overall gain.

1000 reads, 5000 writes

Platform info:
==============
   Darwin 23.3.0 arm64
   Node.JS: 18.20.4
   V8: 10.2.154.26-node.37
   CPU: Apple M2 × 8
   Memory: 16 GB

Suite: serialize
================

✔ msgpack        24,490 ops/sec
✔ json           18,211 ops/sec

   msgpack       +34.48%     (24,490 ops/sec)   (avg: 40μs)
   json (#)           0%     (18,211 ops/sec)   (avg: 54μs)

┌─────────┬────────────────────────────────────────────────────┐
│ msgpack │ ██████████████████████████████████████████████████ │
├─────────┼────────────────────────────────────────────────────┤
│ json    │ █████████████████████████████████████              │
└─────────┴────────────────────────────────────────────────────┘

Note: Careful, as this PR is not revertable. Values set as buffer depends on the code present in this PR to be deserialized.


import { CachableValue, CacheInstance } from './CacheInstance';

const MPACK = new Packr({ moreTypes: true });
Copy link
Contributor Author

@christianblais christianblais Aug 15, 2024

Choose a reason for hiding this comment

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

By enabling moreTypes, we get "free" support for Set and Map.

@@ -212,6 +206,10 @@ export class RedisCache extends CacheInstance {
return false;
}

if (value instanceof Buffer) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this check will work. This function is passed the returned value from a redisClient.get() which will be a string. Incidently, the typing of the function parameter is wrong here 😱 . It should be

public static deserializeValue(value: string | null)

or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Urgh! Right, I tested it with redis directly, but Node's redis lib seems to transform everything into strings. Alright, let me use the string prefix trick, and then I'll rerun the benchmark, to ensure we don't take too much of a performance hit.

@christianblais christianblais changed the title Prefer MSGPack over JSON 💡 [patch] Prefer MSGPack over JSON Aug 19, 2024
@christianblais christianblais marked this pull request as ready for review August 19, 2024 14:20
@christianblais christianblais changed the title 💡 [patch] Prefer MSGPack over JSON 💡 [major] Prefer MSGPack over JSON Aug 19, 2024
Copy link

@ronjouch ronjouch left a comment

Choose a reason for hiding this comment

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

  1. 👍, just a couple nits & questions
  2. Before merge (now), aside local cachette unit tests: did you already run one of our services with this (using npm link), and confirm (with redis-cli) that everything behaves well and looks good?
  3. (If feels valuable after point 2. above) After merge, please test in a staging service with manual redis-cli inspection that everything behaves as expected

src/lib/RedisCache.ts Outdated Show resolved Hide resolved
@@ -72,6 +72,7 @@
},
"dependencies": {
"ioredis": "5.x",
"msgpackr": "1.x",

Choose a reason for hiding this comment

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

@christianblais looks sane and no-dependencies. However, usual introducing-new-depencency questions: how did you pick it? Were there alternatives, and if yes, what led you to think it's the best one?

Copy link
Contributor Author

@christianblais christianblais Aug 19, 2024

Choose a reason for hiding this comment

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

MessagePack is a well defined standard, so the question was — "which implementation should I pick?". There aren't a ton of options to pick from, and this one stands out as the clear winner in terms of love (number/frequency of commits), popularity, and speed.

Screenshot 2024-08-19 at 11 33 58 AM

Now, I'm aware a lib might be feature-complete with no bugs, hence the lack of activity. But looking at the downloads per week, once more, msgpackr stood out as the clear winner. That was the end of my investigation.

Choose a reason for hiding this comment

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

Yip that's what I meant. Okay with your annotated screenshot!

test/RedisCache_test.ts Outdated Show resolved Hide resolved
@@ -212,6 +206,10 @@ export class RedisCache extends CacheInstance {
return false;
}

if (value.startsWith(RedisCache.MSGP_PREFIX)) {
return MPACK.unpack(Buffer.from(value.substring(RedisCache.MSGP_PREFIX.length), 'binary'));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Can you also fix the type of the function param on line 190?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it isn't related to this changeset per se, I did it in another PR.

@christianblais
Copy link
Contributor Author

Locally tested with two of our main services with no apparent issue. So what do you folk say... ship it?

Copy link

@AdelBachene AdelBachene left a comment

Choose a reason for hiding this comment

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

w00t

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.

5 participants