Hermes: Hermes bytecode bundles diff size

Created on 9 Apr 2020  ·  7Comments  ·  Source: facebook/hermes

Hi,

Goal:
I’m creating a binary diff for two consecutive bytecode bundle releases to be used as a patch. Giving an example for clarity, say I have a bytecode bundle released for March - march_bundle.hbc, and one for April - april_bundle.hbc, I’m creating a diff between the two using the library bsdiff. This creates a patch, let’s call it march_april_diff, that can be applied to march_bundle.hbc to get april_bundle.hbc. I’m doing this so that instead of serving the entire april_bundle.hbc (~5 MBs), I can only serve march_april_diff (~800 KBs) and apply it to march_bundle.hbc that resides in the app to get april_bundle.hbc.

Problem:
The size of the diff obtained from the two bytecode bundles is large for my use case and I’m trying to reduce its size.

What I’m trying to do:
I’m trying to debug how the bytecode is generated, in BytecodeModuleGenerator::generate() in BytecodeGenerator.cpp and the preceding files in the call stack. The idea is to modify the bytecode generation in a way so that the diff size reduces. My guess is that some IDs are being attached to modules during bytecode generation that are causing the diff to increase, although I can’t be sure. I’ll be glad if someone can provide some insight or point me in a meaningful direction!

Thanks!

question

Most helpful comment

I tested both methods on two close versions of a 23MB bytecode file:

  • Naive bsdiff: 2.9 MB
  • bsdiff with deltaprep: 1.9 MB
  • bsdiff with --base-bytecode: 1.0 MB
  • bsdiff with both: 0.5 MB

--base-bytecode is both the most impactful and the easiest to set up (since it requires no post-processing). The final bundle was just 0.006 MB larger.

All 7 comments

Please take a look at the --base-bytecode compiler command line option. It allows you to specify a previous bytecode image when generating the new one, which should result in the new one being more diffable.

As another approach, we also have a tool called hbc-deltaprep, under tools/hbc-deltaprep. It converts the bytecode from and to a form that is inherently more diffable. That tool is not actively supported by us though.

I tested both methods on two close versions of a 23MB bytecode file:

  • Naive bsdiff: 2.9 MB
  • bsdiff with deltaprep: 1.9 MB
  • bsdiff with --base-bytecode: 1.0 MB
  • bsdiff with both: 0.5 MB

--base-bytecode is both the most impactful and the easiest to set up (since it requires no post-processing). The final bundle was just 0.006 MB larger.

Thanks @tmikov @willholen, I tried -base-bytecode and the size reduced to 340 KBs which is pretty amazing. Will try hbc-deltaprep as well. I’ll be glad if either of you can share what -base-bytecode and hbc-deltaprep actually do to reduce the diff size.

Also as a suggestion for future users, it’ll be great if a doc can be provided for a more detailed explanation of the command line options and tools.

Thanks again!

Hermes bytecode contains a string table, and all instructions using string literals or identifiers will reference an index into it. Minor changes to the JavaScript tend to cause all the indices to shift, thereby causing cascading changes to every such instruction.

-base-bytecode initializes the string/identifier table with that of the base bytecode file, so that these cascading shifts don't happen. The VM doesn't care how the string table is ordered, so this is a transparent change.

In addition to string indices, there are various other headers and tables, such as for function offsets and array buffers. These suffer from similar cascading issues, where e.g. adding a 32 byte function will cause all subsequent function offsets to increase by 32, making diffs larger.

hbc-deltaprep will essentially rewrite these indices from absolute to relative values to improve diffability. The VM can't work with these relative indices, so a separate and opposite post-processing step is required to rewrite them back into absolute values.

These are indeed underdocumented features.

I also facing this problem and find -base-byteocde option is useful,but I also has a question and hope anyone to give me a answer :)

These is my test table:

  • base 1 version(method 1) means all the version base on a original version. e.g. There are the first version named 1.hbc, and 2.hbc3.hbc compiled base on 1.hbc
  • hbc base last version(method 2) means the next version base on the last version. e.g. There are the first version named 1.hbc, and 2.hbc compiled base on 1.hbc, 3.hbc compiled base on 2.hbc.
  • jsbundle size: 4M
  • hbc size: 3M (-O -output-source-map)

Table.1:

| Code Change | js | hbc | hbc(base 1 version) | hbc base last version |
|---------|------|--------|------|--------|
| only change string | 306b | 130kb(400 times)| 389B(almost same as js diff) | 2kb (7 times) |
| about 50+ files modify | 14kb | 400kb(28 times)| 88kb(6 times) | 86kb(6 times) |
| about 100+files modify| 25kb | 458kb (18times)| 152kb(6 times) | 151kb(6 times) |
| about 170+ file modify | 46kb | 561kb (12 times) | 201kb(4.5 times) | 202kb(4.5 times)|

Table.2:

  • 1 -> 13 means version 1 diff with version 13

| version diff | js | Hbc | Hbc(base 1 version) | Hbc(base last version) |
|----------|------|-------------|---------------------|------------------------|
| 1 -> 13 | 27kb | 489kb(18 times) | 160kb (6 times) | 160kb (6 times) |
| 13 -> 24 | 12kb | 432kb(36 times) | 126kb (10.5 times) | 77kb (6.4 times) |
| 24 -> 30 | 14kb | 400kb(28 times) | 88kb (6 times) | 86kb (6 times) |
| 30 -> 31 | 306b | 133kb(400 times) | 389B (almost same as js diff) | 2kb (6 times) |

According to the two tables, I can draw the conclusion:

  1. enable base-bytecode option can reduce diff size (almost 6 times)
  2. It is a little difference between compilebase on a same version(method 1) and base on the last version(method 2).

    • In only change string case, method 1 s result(almost close to jsbundle diff result ) is much smaller thanmethod 2`. (Why?)

    • When Diff between two close version,method 2 is better than method 1, expect only change string.

  3. It is also a little shortcoming,enable this option will cause the hbc bundle size to increase. e.g. In version 31, method 1 is 4.7k larger than origin one, and method 2 is 5.2kb larger than origin one.

If you have any question of my research, please remind me fell free~
Forigive my poor english OTL, @willholen @tmikov hope you can help me out~

Try hbc-deltaprep tools with this commad line:

./hbc-deltaprep ./index.hbc -out delta.hbc -form delta

to produce delta object.

Then use:
./hbc-deltaprep ./delta.hbc -out index2.hbc -form execution

convert back to index2.hbc

When I run the index2.hbc on my phone, it crash, but index.hbc is fine . I also notice the md5 is different between index.hbc and index2.hbc.

Is there something wrong with my operation?

@SinLucifer This looks like a bug. As mentioned, we don't actively support or use this experimental tool.

I filed a separate issue #211 about it.

Was this page helpful?
0 / 5 - 0 ratings