├── .gitignore ├── package.json ├── _config.yml ├── html ├── index.template.html └── benchmark.template.html ├── js ├── marker │ ├── end.js │ ├── start.js │ ├── end │ │ ├── modules.js │ │ └── preload.js │ └── start │ │ ├── modules.js │ │ └── preload.js └── helper.js ├── CONTRIBUTING.md ├── README.md ├── .github └── workflows │ └── update.yml ├── LICENSE └── gen.py /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | .vscode 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-benchmark", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "rollup": "^2.35.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - jekyll-relative-links 3 | relative_links: 4 | enabled: true 5 | collections: true 6 | include: 7 | - CONTRIBUTING.md 8 | - README.md 9 | 10 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /html/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MODULE BENCHMARKS 5 | 6 | 7 |

Module Loading Benchmarks

8 |

Info

9 | $info 10 | 11 |

Variants

12 | 15 | 16 | -------------------------------------------------------------------------------- /js/marker/end.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | timeEnd('main'); 16 | -------------------------------------------------------------------------------- /js/marker/start.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | timeStart('main'); 16 | -------------------------------------------------------------------------------- /html/benchmark.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MODULE BENCHMARK 5 | 6 | $headers 7 | 8 | 9 |

Module Loading Benchmark

10 |

Info

11 | $info 12 | 13 |

Results

14 |

15 | 
16 |   
17 |   
18 |   
21 |   $scripts
22 |   
25 |   
26 | 
27 | 


--------------------------------------------------------------------------------
/js/marker/end/modules.js:
--------------------------------------------------------------------------------
 1 | // Copyright 2020 Google LLC
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //     https://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | timeEnd('modules');
16 | 


--------------------------------------------------------------------------------
/js/marker/end/preload.js:
--------------------------------------------------------------------------------
 1 | // Copyright 2020 Google LLC
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //     https://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | timeEnd('preload');
16 | 


--------------------------------------------------------------------------------
/js/marker/start/modules.js:
--------------------------------------------------------------------------------
 1 | // Copyright 2020 Google LLC
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //     https://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | timeStart('modules');
16 | 


--------------------------------------------------------------------------------
/js/marker/start/preload.js:
--------------------------------------------------------------------------------
 1 | // Copyright 2020 Google LLC
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //     https://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | timeStart('preload');
16 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # How to Contribute
 2 | 
 3 | We'd love to accept your patches and contributions to this project. There are
 4 | just a few small guidelines you need to follow.
 5 | 
 6 | ## Contributor License Agreement
 7 | 
 8 | Contributions to this project must be accompanied by a Contributor License
 9 | Agreement (CLA). You (or your employer) retain the copyright to your
10 | contribution; this simply gives us permission to use and redistribute your
11 | contributions as part of the project. Head over to
12 |  to see your current agreements on file or
13 | to sign a new one.
14 | 
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 | 
19 | ## Code reviews
20 | 
21 | All submissions, including submissions by project members, require review. We
22 | use GitHub pull requests for this purpose. Consult
23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
24 | information on using pull requests.
25 | 
26 | ## Community Guidelines
27 | 
28 | This project follows
29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
30 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Synthetic Module Benchmark
 2 | 
 3 | ## Installation
 4 | To run this benchmark you need python and a local webserver, for instance
 5 | [local-web-server](https://www.npmjs.com/package/local-web-server) does the
 6 | trick.
 7 | 
 8 | ## Basic Usage
 9 | 1. Generate the modules: `rm -rf out/ && python3 ./gen.py`
10 | 2. Start webserver: `ws`
11 | 3. Run the benchmark:
12 |   ```
13 |   testing/xvfb.py out/release/chrome http://localhost:8000/out/prefetch-0.html
14 |   ```
15 | 
16 | The `out/` directory contains various version of the main app with different
17 | degrees of module [prefetching](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/modulepreload) and top-level includes.
18 | 
19 | ## Extended Usage
20 | `gen.py` can be used with simple expansion rules to generate arbitrary trees.
21 | Let's consider the following simple tree:
22 | 
23 | ```
24 |    A
25 |  / | \
26 | B  A  C
27 |  / | \
28 | B  A  C
29 | ```
30 | 
31 | It can be generated with `./get.py --depth=2 --rules="A:BAC"`.
32 | Notes: 
33 |  - `A` is the default starting axiom.
34 |  - A module name consist of a single character other than `,` and `:`.
35 | 
36 | Additionally you can specify the size of each generated module with `--sizes`:
37 | 
38 | ```
39 | ./get.py --depth=2 --rules="A:AB" --sizes="A:12k,B:1m,C:1k"
40 | ```
41 | ## Examples
42 | 
43 | * [~250 small modules](./light-left): `--depth=70 --rules="A:ABCD" --sizes="A:1k,B:1k,C:1k,D:1k"`
44 | * [~250 small modules](./light-right): `--depth=70 --rules="A:BCDA" --sizes="A:1k,B:1k,C:1k,D:1k"`
45 | * [~1200 very small modules](./light-mixed): `--depth=8 --rules="A:ABCDA"`
46 | * [~210 large modules](./heavy-middle): `--depth=70 --rules="A:BAC" --sizes="A:100k,B:1m,C:300k"`
47 | 


--------------------------------------------------------------------------------
/js/helper.js:
--------------------------------------------------------------------------------
 1 | // Copyright 2020 Google LLC
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //     https://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | function hasURLParam(key) {
16 |   const queryString = window.location.search;
17 |   const urlParams = new URLSearchParams(queryString);
18 |   return urlParams.has(key)
19 | }
20 | 
21 | async function runModuleBenchmark(module_promise) {
22 |   const module = await module_promise;
23 |   timeEnd('loadModules');
24 |   log(new Date().toISOString(), "Loaded: A.mjs");
25 | 
26 |   const kCount = 5;
27 |   log("\nEvaluating imports only:".toUpperCase())
28 |   document.evaluate_all = false
29 |   for (let i=1; i<=kCount; i++) {
30 |     const label = `Iteration ${i}: evaluate f_A()`;
31 |     timeStart(label)
32 |     log('    f_A() result=', await module.f_A());
33 |     timeEnd(label);
34 |   }
35 | 
36 |   log("\nEvaluating all code:".toUpperCase())
37 |   document.evaluate_all = true
38 |   for (let i=1; i<=kCount; i++) {
39 |     const label = `Iteration ${i}: evaluate all f_A()`;
40 |     timeStart(label);
41 |     log('    f_A() result=', await module.f_A());
42 |     timeEnd(label);
43 |   }
44 | }
45 | 
46 | function log(...msg) {
47 |   console.log(...msg);
48 |   document.getElementById('log').innerText += `${msg.join(' ')}\n`;
49 | }
50 | 
51 | function timeStart(label) {
52 |   const date = new Date().toISOString();
53 |   log(`${label} start: ${date}`);
54 |   console.time(label);
55 |   performance.mark(`${label}-start`);
56 | }
57 | 
58 | function timeEnd(label) {
59 |   performance.mark(`${label}-end`);
60 |   console.timeEnd(label);
61 |   performance.measure(label, `${label}-start`, `${label}-end`);
62 |   const time = performance.getEntriesByName(label)[0].duration;
63 |   log(`${label} duration: ${time.toFixed(2)}ms`);
64 | }
65 | 


--------------------------------------------------------------------------------
/.github/workflows/update.yml:
--------------------------------------------------------------------------------
 1 | # Copyright 2020 the V8 project authors. All rights reserved.
 2 | # Use of this source code is governed by a BSD-style license that can be
 3 | # found in the LICENSE file.
 4 | 
 5 | name: Deploy Modules Benchmark
 6 | 
 7 | on:
 8 |   push:
 9 |     branches:
10 |       - main
11 |   workflow_dispatch:
12 | 
13 | jobs:
14 |   build:
15 |     name: Build and Deploy 
16 |     runs-on: ubuntu-latest
17 |     steps:
18 |       - name: Checkout main Branch 
19 |         uses: actions/checkout@v2
20 |         with:
21 |           persist-credentials: false
22 | 
23 |       - name: Install node 
24 |         uses: actions/setup-node@v2
25 |         with:
26 |           node-version: '14'
27 |       
28 |       - name: 'Install node packages'
29 |         run: npm install
30 | 
31 |       - name: Export Module Tree
32 |         run: |
33 |             python3 ./gen.py --depth=70 --rules="A:ABCD" --sizes="A:1k,B:1k,C:1k,D:1k"
34 |             mv out light-left
35 |             python3 ./gen.py --depth=70 --rules="A:ABCD" --sizes="A:1k,B:1k,C:1k,D:1k" --dynamic-imports
36 |             mv out light-left-dynamic
37 |       
38 |       - name: Export Module Tree
39 |         run: |
40 |             python3 ./gen.py --depth=70 --rules="A:BCDA" --sizes="A:1k,B:1k,C:1k,D:1k"
41 |             mv out light-right
42 |             python3 ./gen.py --depth=70 --rules="A:BCDA" --sizes="A:1k,B:1k,C:1k,D:1k" --dynamic-imports
43 |             mv out light-right-dynamic
44 |      
45 |       - name: Export Module Tree
46 |         run: |
47 |             python3 ./gen.py --depth=8 --rules="A:ABCDA" --sizes="A:1k,B:1k,C:1k,D:1k"
48 |             mv out light-mixed
49 |             python3 ./gen.py --depth=8 --rules="A:ABCDA" --sizes="A:1k,B:1k,C:1k,D:1k" --dynamic-imports
50 |             mv out light-mixed-dynamic
51 |      
52 |       - name: Export Module Tree
53 |         run: |
54 |             python3 ./gen.py --depth=20 --rules="A:BAC" --sizes="A:100k,B:1m,C:300k"
55 |             mv out heavy-middle
56 |             python3 ./gen.py --depth=20 --rules="A:BAC" --sizes="A:100k,B:1m,C:300k" --dynamic-imports
57 |             mv out heavy-middle-dynamic
58 | 
59 |       - name: Deploy
60 |         uses: JamesIves/github-pages-deploy-action@3.7.1
61 |         with:
62 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 |           BRANCH: gh-pages
64 |           FOLDER: .
65 |           CLEAN: true
66 |           SINGLE_COMMIT: true
67 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 | 
  2 |                                  Apache License
  3 |                            Version 2.0, January 2004
  4 |                         http://www.apache.org/licenses/
  5 | 
  6 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7 | 
  8 |    1. Definitions.
  9 | 
 10 |       "License" shall mean the terms and conditions for use, reproduction,
 11 |       and distribution as defined by Sections 1 through 9 of this document.
 12 | 
 13 |       "Licensor" shall mean the copyright owner or entity authorized by
 14 |       the copyright owner that is granting the License.
 15 | 
 16 |       "Legal Entity" shall mean the union of the acting entity and all
 17 |       other entities that control, are controlled by, or are under common
 18 |       control with that entity. For the purposes of this definition,
 19 |       "control" means (i) the power, direct or indirect, to cause the
 20 |       direction or management of such entity, whether by contract or
 21 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 22 |       outstanding shares, or (iii) beneficial ownership of such entity.
 23 | 
 24 |       "You" (or "Your") shall mean an individual or Legal Entity
 25 |       exercising permissions granted by this License.
 26 | 
 27 |       "Source" form shall mean the preferred form for making modifications,
 28 |       including but not limited to software source code, documentation
 29 |       source, and configuration files.
 30 | 
 31 |       "Object" form shall mean any form resulting from mechanical
 32 |       transformation or translation of a Source form, including but
 33 |       not limited to compiled object code, generated documentation,
 34 |       and conversions to other media types.
 35 | 
 36 |       "Work" shall mean the work of authorship, whether in Source or
 37 |       Object form, made available under the License, as indicated by a
 38 |       copyright notice that is included in or attached to the work
 39 |       (an example is provided in the Appendix below).
 40 | 
 41 |       "Derivative Works" shall mean any work, whether in Source or Object
 42 |       form, that is based on (or derived from) the Work and for which the
 43 |       editorial revisions, annotations, elaborations, or other modifications
 44 |       represent, as a whole, an original work of authorship. For the purposes
 45 |       of this License, Derivative Works shall not include works that remain
 46 |       separable from, or merely link (or bind by name) to the interfaces of,
 47 |       the Work and Derivative Works thereof.
 48 | 
 49 |       "Contribution" shall mean any work of authorship, including
 50 |       the original version of the Work and any modifications or additions
 51 |       to that Work or Derivative Works thereof, that is intentionally
 52 |       submitted to Licensor for inclusion in the Work by the copyright owner
 53 |       or by an individual or Legal Entity authorized to submit on behalf of
 54 |       the copyright owner. For the purposes of this definition, "submitted"
 55 |       means any form of electronic, verbal, or written communication sent
 56 |       to the Licensor or its representatives, including but not limited to
 57 |       communication on electronic mailing lists, source code control systems,
 58 |       and issue tracking systems that are managed by, or on behalf of, the
 59 |       Licensor for the purpose of discussing and improving the Work, but
 60 |       excluding communication that is conspicuously marked or otherwise
 61 |       designated in writing by the copyright owner as "Not a Contribution."
 62 | 
 63 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 64 |       on behalf of whom a Contribution has been received by Licensor and
 65 |       subsequently incorporated within the Work.
 66 | 
 67 |    2. Grant of Copyright License. Subject to the terms and conditions of
 68 |       this License, each Contributor hereby grants to You a perpetual,
 69 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 70 |       copyright license to reproduce, prepare Derivative Works of,
 71 |       publicly display, publicly perform, sublicense, and distribute the
 72 |       Work and such Derivative Works in Source or Object form.
 73 | 
 74 |    3. Grant of Patent License. Subject to the terms and conditions of
 75 |       this License, each Contributor hereby grants to You a perpetual,
 76 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 77 |       (except as stated in this section) patent license to make, have made,
 78 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 79 |       where such license applies only to those patent claims licensable
 80 |       by such Contributor that are necessarily infringed by their
 81 |       Contribution(s) alone or by combination of their Contribution(s)
 82 |       with the Work to which such Contribution(s) was submitted. If You
 83 |       institute patent litigation against any entity (including a
 84 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 85 |       or a Contribution incorporated within the Work constitutes direct
 86 |       or contributory patent infringement, then any patent licenses
 87 |       granted to You under this License for that Work shall terminate
 88 |       as of the date such litigation is filed.
 89 | 
 90 |    4. Redistribution. You may reproduce and distribute copies of the
 91 |       Work or Derivative Works thereof in any medium, with or without
 92 |       modifications, and in Source or Object form, provided that You
 93 |       meet the following conditions:
 94 | 
 95 |       (a) You must give any other recipients of the Work or
 96 |           Derivative Works a copy of this License; and
 97 | 
 98 |       (b) You must cause any modified files to carry prominent notices
 99 |           stating that You changed the files; and
100 | 
101 |       (c) You must retain, in the Source form of any Derivative Works
102 |           that You distribute, all copyright, patent, trademark, and
103 |           attribution notices from the Source form of the Work,
104 |           excluding those notices that do not pertain to any part of
105 |           the Derivative Works; and
106 | 
107 |       (d) If the Work includes a "NOTICE" text file as part of its
108 |           distribution, then any Derivative Works that You distribute must
109 |           include a readable copy of the attribution notices contained
110 |           within such NOTICE file, excluding those notices that do not
111 |           pertain to any part of the Derivative Works, in at least one
112 |           of the following places: within a NOTICE text file distributed
113 |           as part of the Derivative Works; within the Source form or
114 |           documentation, if provided along with the Derivative Works; or,
115 |           within a display generated by the Derivative Works, if and
116 |           wherever such third-party notices normally appear. The contents
117 |           of the NOTICE file are for informational purposes only and
118 |           do not modify the License. You may add Your own attribution
119 |           notices within Derivative Works that You distribute, alongside
120 |           or as an addendum to the NOTICE text from the Work, provided
121 |           that such additional attribution notices cannot be construed
122 |           as modifying the License.
123 | 
124 |       You may add Your own copyright statement to Your modifications and
125 |       may provide additional or different license terms and conditions
126 |       for use, reproduction, or distribution of Your modifications, or
127 |       for any such Derivative Works as a whole, provided Your use,
128 |       reproduction, and distribution of the Work otherwise complies with
129 |       the conditions stated in this License.
130 | 
131 |    5. Submission of Contributions. Unless You explicitly state otherwise,
132 |       any Contribution intentionally submitted for inclusion in the Work
133 |       by You to the Licensor shall be under the terms and conditions of
134 |       this License, without any additional terms or conditions.
135 |       Notwithstanding the above, nothing herein shall supersede or modify
136 |       the terms of any separate license agreement you may have executed
137 |       with Licensor regarding such Contributions.
138 | 
139 |    6. Trademarks. This License does not grant permission to use the trade
140 |       names, trademarks, service marks, or product names of the Licensor,
141 |       except as required for reasonable and customary use in describing the
142 |       origin of the Work and reproducing the content of the NOTICE file.
143 | 
144 |    7. Disclaimer of Warranty. Unless required by applicable law or
145 |       agreed to in writing, Licensor provides the Work (and each
146 |       Contributor provides its Contributions) on an "AS IS" BASIS,
147 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 |       implied, including, without limitation, any warranties or conditions
149 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 |       PARTICULAR PURPOSE. You are solely responsible for determining the
151 |       appropriateness of using or redistributing the Work and assume any
152 |       risks associated with Your exercise of permissions under this License.
153 | 
154 |    8. Limitation of Liability. In no event and under no legal theory,
155 |       whether in tort (including negligence), contract, or otherwise,
156 |       unless required by applicable law (such as deliberate and grossly
157 |       negligent acts) or agreed to in writing, shall any Contributor be
158 |       liable to You for damages, including any direct, indirect, special,
159 |       incidental, or consequential damages of any character arising as a
160 |       result of this License or out of the use or inability to use the
161 |       Work (including but not limited to damages for loss of goodwill,
162 |       work stoppage, computer failure or malfunction, or any and all
163 |       other commercial damages or losses), even if such Contributor
164 |       has been advised of the possibility of such damages.
165 | 
166 |    9. Accepting Warranty or Additional Liability. While redistributing
167 |       the Work or Derivative Works thereof, You may choose to offer,
168 |       and charge a fee for, acceptance of support, warranty, indemnity,
169 |       or other liability obligations and/or rights consistent with this
170 |       License. However, in accepting such obligations, You may act only
171 |       on Your own behalf and on Your sole responsibility, not on behalf
172 |       of any other Contributor, and only if You agree to indemnify,
173 |       defend, and hold each Contributor harmless for any liability
174 |       incurred by, or claims asserted against, such Contributor by reason
175 |       of your accepting any such warranty or additional liability.
176 | 
177 |    END OF TERMS AND CONDITIONS
178 | 
179 |    APPENDIX: How to apply the Apache License to your work.
180 | 
181 |       To apply the Apache License to your work, attach the following
182 |       boilerplate notice, with the fields enclosed by brackets "[]"
183 |       replaced with your own identifying information. (Don't include
184 |       the brackets!)  The text should be enclosed in the appropriate
185 |       comment syntax for the file format. We also recommend that a
186 |       file or class name and description of purpose be included on the
187 |       same "printed page" as the copyright notice for easier
188 |       identification within third-party archives.
189 | 
190 |    Copyright [yyyy] [name of copyright owner]
191 | 
192 |    Licensed under the Apache License, Version 2.0 (the "License");
193 |    you may not use this file except in compliance with the License.
194 |    You may obtain a copy of the License at
195 | 
196 |        http://www.apache.org/licenses/LICENSE-2.0
197 | 
198 |    Unless required by applicable law or agreed to in writing, software
199 |    distributed under the License is distributed on an "AS IS" BASIS,
200 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 |    See the License for the specific language governing permissions and
202 |    limitations under the License.
203 | 


--------------------------------------------------------------------------------
/gen.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | # -*- coding: utf-8 -*-"
  3 | 
  4 | # Copyright 2020 Google LLC
  5 | #
  6 | # Licensed under the Apache License, Version 2.0 (the "License");
  7 | # you may not use this file except in compliance with the License.
  8 | # You may obtain a copy of the License at
  9 | #
 10 | #     https://www.apache.org/licenses/LICENSE-2.0
 11 | #
 12 | # Unless required by applicable law or agreed to in writing, software
 13 | # distributed under the License is distributed on an "AS IS" BASIS,
 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15 | # See the License for the specific language governing permissions and
 16 | # limitations under the License.
 17 | 
 18 | from optparse import OptionParser
 19 | from pathlib import Path
 20 | import subprocess
 21 | import math
 22 | import random
 23 | from string import Template
 24 | import functools
 25 | 
 26 | #=============================================================================
 27 | 
 28 | ROOT_AXIOM = 'A'
 29 | 
 30 | #=============================================================================
 31 | 
 32 | 
 33 | class Options(object):
 34 |   def __init__(self):
 35 |     self.setup_option_parser()
 36 |     self.parse_options()
 37 |     self.validate_options()
 38 | 
 39 |   def setup_option_parser(self):
 40 |     self.parser = OptionParser("usage: %prog")
 41 |     self.parser.add_option("-d",
 42 |                            "--depth",
 43 |                            type="int",
 44 |                            default=4,
 45 |                            help="Specifies the module tree depth, default=4")
 46 |     self.parser.add_option(
 47 |         "-b",
 48 |         "--branches",
 49 |         type="int",
 50 |         default=10,
 51 |         help="Specifies the module tree width at each level, default=10")
 52 |     self.parser.add_option(
 53 |         "--rules",
 54 |         default=None,
 55 |         help="Specifies l_system (for instance 'A:AB,B:BBB') rules to "
 56 |         "generate the inclusion. "
 57 |         "By default the script auto-generates a balanced tree."
 58 |         "The start axiom is always 'A'."
 59 |         "The rule expansion happens DEPTH times.")
 60 |     self.parser.add_option(
 61 |         "--sizes",
 62 |         default=None,
 63 |         help="Specify a size for each module, for instance 'A:10K,B:1.4M'"
 64 |         "This is the generated module payload and does not include "
 65 |         "module setup and import code for now.")
 66 |     self.parser.add_option("--dynamic-imports",
 67 |                            default=False,
 68 |                            action="store_true",
 69 |                            help="Use dynamic imports everywhere.")
 70 | 
 71 |   def parse_options(self):
 72 |     (self.options, args) = self.parser.parse_args()
 73 | 
 74 |   def validate_options(self):
 75 |     if self.options.depth < 0:
 76 |       self.parser.error("--depth has to be >= 0")
 77 |     if self.options.branches <= 0:
 78 |       self.parser.error("--branches has to be >= 1")
 79 | 
 80 |     if self.options.rules is None:
 81 |       self.options.rules = "A:" + "A" * self.options.branches
 82 |     self.parse_rules()
 83 | 
 84 |     self.parse_sizes()
 85 | 
 86 |   def parse_rules(self):
 87 |     rules = dict()
 88 |     for rule in self.options.rules.split(','):
 89 |       predecessor, successors = rule.split(':')
 90 |       if len(predecessor) != 1:
 91 |         self.parser.error(
 92 |             f"Invalid rule '{rule}'': only 1 char predecessor allowed")
 93 |       if len(successors) == 0:
 94 |         self.parser.error(f"Invalid rule '{rule}': successor not specified")
 95 |       if predecessor in rules:
 96 |         self.parser.error(f"More than one rule for '{predecessor}")
 97 |       rules[predecessor] = successors
 98 |     if 'A' not in rules:
 99 |       self.parser.error("Missing rule for initial axiom 'A'")
100 |     print("RULES: ", rules)
101 |     self.options.rules = rules
102 | 
103 |   def parse_sizes(self):
104 |     input_sizes = self.options.sizes
105 |     sizes = self.options.sizes = dict()
106 |     if input_sizes is None:
107 |       return
108 |     for input in input_sizes.split(','):
109 |       symbol, size_string = input.split(':')
110 |       size_string = size_string.lower()
111 |       if size_string.endswith('k'):
112 |         size = int(size_string[:-1]) * 1024
113 |       elif size_string.endswith('m'):
114 |         size = int(size_string[:-1]) * 1024 * 1024
115 |       else:
116 |         size = int(size_string) * 1024
117 |       if symbol in sizes:
118 |         self.parser.error(f"Duplicate size for {symbol}")
119 |       sizes[symbol] = size
120 |     print("SIZES: ", self.options.sizes)
121 | 
122 | 
123 | #=============================================================================
124 | 
125 | 
126 | class Module(object):
127 |   def __init__(self,
128 |                name,
129 |                parent,
130 |                depth,
131 |                index=0,
132 |                size=0,
133 |                dynamic_import=False):
134 |     self.children = []
135 |     self.simple_name = name
136 |     self.parent = parent
137 |     self.depth = depth
138 |     self.index = index
139 |     self.size = int(size)
140 |     self.dynamic_import = dynamic_import
141 |     if parent:
142 |       # Create unique name based on full path
143 |       path = self.parent.submodule_path / name
144 |       self.name = str(path).replace("/", "_")
145 |       self.path = self.parent.submodule_path / (self.name + ".mjs")
146 |     else:
147 |       self.name = self.simple_name
148 |       self.path = Path(self.name + ".mjs")
149 |     self.submodule_path = self.path.parent / self.simple_name
150 | 
151 |   def append(self, module):
152 |     self.children.append(module)
153 | 
154 |   def has_children(self):
155 |     return len(self.children) > 0
156 | 
157 |   def expand_recurse(self, symbol, options, accumulator):
158 |     if self.depth == options.depth:
159 |       return
160 |     if symbol not in options.rules:
161 |       return
162 |     expansion = options.rules[symbol]
163 |     index = 0
164 |     for symbol in expansion:
165 |       name = f"{symbol}{index}"
166 |       module = Module(name,
167 |                       parent=self,
168 |                       depth=self.depth + 1,
169 |                       index=index,
170 |                       size=options.sizes.get(symbol, 0),
171 |                       dynamic_import=options.dynamic_imports)
172 |       index += 1
173 |       self.append(module)
174 |       accumulator.append(module)
175 |       module.expand_recurse(symbol, options, accumulator)
176 |     return accumulator
177 | 
178 |   def export(self, out_dir):
179 |     with (out_dir / self.path).open('w') as f:
180 |       operations = []
181 |       operations = self.write_imports(f)
182 |       if self.dynamic_import:
183 |         f.write(f"export async function f_{self.name}() {{\n")
184 |       else:
185 |         f.write(f"export function f_{self.name}() {{\n")
186 |       f.write("  let a=1;\n")
187 |       if self.size:
188 |         f.write("  if (document.evaluate_all) {\n")
189 |         f.write("    a=helper()\n")
190 |         f.write("  }\n")
191 |       if self.dynamic_import:
192 |         f.write(f"  const results = await Promise.all([\n")
193 |         operations = ",\n    ".join(operations)
194 |         f.write(f"    {operations}\n")
195 |         f.write("  ]);\n")
196 |         f.write("  for (let result of results) a += result;\n")
197 |         f.write("  return a;\n")
198 |       else:
199 |         operations.append('a')
200 |         operations = "+".join(operations)
201 |         f.write(f"  return {operations};\n")
202 |       f.write("}\n")
203 |       if self.size:
204 |         f.write("function helper() {\n")
205 |         f.write("  let a=1;")
206 |         # Use random numbers to increase entropy and lower compression quality.
207 |         # The number of random digits dials in the average compression rate:
208 |         # - 10 digits => ~3x
209 |         # -  3 digits => ~5x
210 |         # -  2 digits => ~7x
211 |         random_digits = 10
212 |         instruction_length = (3 + random_digits + 1) + (3 + random_digits + 2)
213 |         min_range = 10**(random_digits - 1)
214 |         max_range = 10**random_digits - 1
215 |         for i in range(
216 |             math.floor(
217 |                 (self.size - len(operations) * 2) / instruction_length)):
218 |           number = random.randint(min_range, max_range)
219 |           f.write(f"a+={number};a-={number};\n")
220 |         f.write(";\n")
221 |         f.write("  return a+100;\n")
222 |         f.write("}\n")
223 | 
224 |     if self.has_children():
225 |       (out_dir / self.submodule_path).mkdir()
226 |       for module in self.children:
227 |         module.export(out_dir)
228 | 
229 |   def write_imports(self, f):
230 |     if self.dynamic_import:
231 |       return self.create_dynamic_imports()
232 |     else:
233 |       return self.write_static_imports(f)
234 | 
235 |   def create_dynamic_imports(self):
236 |     return [
237 |         f"import('./{self.simple_name}/{module.name}.mjs').then(m => m.f_{module.name}())"
238 |         for module in self.children
239 |     ]
240 | 
241 |   def write_static_imports(self, f):
242 |     operations = []
243 |     for module in self.children:
244 |       f.write(
245 |           f"import {{f_{module.name}}} from './{self.simple_name}/{module.name}.mjs'\n"
246 |       )
247 |       operations.append(f"f_{module.name}()")
248 |     return operations
249 | 
250 | 
251 | #=============================================================================
252 | 
253 | 
254 | def step(comment):
255 |   def step_decorator(func):
256 |     @functools.wraps(func)
257 |     def step_wrapper(*args, **kwargs):
258 |       print(f'::group::{comment}')
259 |       result = func(*args, **kwargs)
260 |       print('::endgroup::')
261 |       return result
262 | 
263 |     return step_wrapper
264 | 
265 |   return step_decorator
266 | 
267 | 
268 | class Benchmark(object):
269 |   def __init__(self, options):
270 |     self.options = options
271 |     self.create_module_tree()
272 | 
273 |   @functools.lru_cache(maxsize=10)
274 |   def template(self, name):
275 |     template_path = Path(__file__).parent / 'html' / f"{name}.template.html"
276 |     with open(template_path, 'r') as template_file:
277 |       return Template(template_file.read())
278 | 
279 |   def benchmark_template(self):
280 |     return self.template('benchmark')
281 | 
282 |   @step("Creating module tree")
283 |   def create_module_tree(self):
284 |     self.options.module_tree = []
285 |     self.start_module = Module(ROOT_AXIOM,
286 |                                parent=None,
287 |                                depth=0,
288 |                                size=0,
289 |                                dynamic_import=self.options.dynamic_imports)
290 |     self.modules = self.start_module.expand_recurse(ROOT_AXIOM,
291 |                                                     self.options,
292 |                                                     accumulator=[])
293 | 
294 |   @step("Exporting benchmark")
295 |   def export(self, out_path):
296 |     out_path.mkdir(parents=True, exist_ok=True)
297 |     self.export_modules(out_path)
298 |     if not self.options.dynamic_imports:
299 |       self.build_bundles(out_path)
300 |     self.export_html(out_path)
301 | 
302 |   @step("Exporting modules")
303 |   def export_modules(self, out_path):
304 |     self.start_module.export(out_path)
305 |     print(f" Created {len(self.modules )} modules")
306 | 
307 |     # Topologically sort paths
308 |     self.modules.sort(key=lambda m: (len(m.path.parts), m.path.parts))
309 | 
310 |   @step("Building bundles")
311 |   def build_bundles(self, out_path):
312 |     module_path = out_path / "A.mjs"
313 |     bundle_path = out_path / 'bundled.mjs'
314 |     subprocess.check_call(
315 |         f"npx rollup '{ module_path}' --format=esm --file='{bundle_path}' --name=A",
316 |         shell=True)
317 |     # TODO: support webbundle
318 | 
319 |   @step("Exporting html")
320 |   def export_html(self, out_path):
321 |     benchmarks = []
322 |     for count in (0, 10, 50, 100, 250, 500, len(self.modules)):
323 |       benchmarks.append(
324 |           self.export_benchmark(out_path, 'prefetch', prefetch_count=count))
325 |     for count in (0, 10, 50, 100, 250, 500, len(self.modules)):
326 |       benchmarks.append(
327 |           self.export_benchmark(out_path, 'preload', preload_count=count))
328 |     if not self.options.dynamic_imports:
329 |       benchmarks.append(self.export_bundled(out_path))
330 |     self.export_index(out_path, benchmarks)
331 | 
332 |   @step("Exporting bundled")
333 |   def export_bundled(self, out_path):
334 |     path = out_path / 'bundled.html'
335 |     with open(path, 'w') as f:
336 |       f.write(self.benchmark_template().substitute(
337 |           dict(headers="",
338 |                info=self.output_info(),
339 |                scripts="",
340 |                module='./bundled.mjs')))
341 |     return path
342 | 
343 |   def export_benchmark(self,
344 |                        out_path,
345 |                        name,
346 |                        prefetch_count=0,
347 |                        preload_count=0,
348 |                        wait=0):
349 |     file_name = f'{name}-{prefetch_count+preload_count}.html'
350 |     print(f"  {file_name}")
351 |     path = out_path / file_name
352 |     with open(path, 'w') as f:
353 |       f.write(self.benchmark_template().substitute(
354 |           dict(headers=self.output_headers(prefetch_count),
355 |                info=self.output_info(),
356 |                scripts=self.output_scripts(preload_count),
357 |                module='./A.mjs')))
358 |     return path
359 | 
360 |   def output_headers(self, prefetch_count):
361 |     return "\n".join([
362 |         f'  '
363 |         for module in self.modules[:prefetch_count]
364 |     ])
365 | 
366 |   def output_info(self):
367 |     return f"""
368 |   
369 |
Expansion Rules:
370 |
{self.options.rules}
371 |
Module Sizes:
372 |
{self.options.sizes}
373 |
Depth:
374 |
{self.options.depth}
375 |
Module Count:
376 |
{len(self.modules)}
377 |
Total Source Size:
378 |
{round(sum(map(lambda m: m.size, self.modules))/1024/1024, 2)} MiB
379 |
""" 380 | 381 | def output_scripts(self, preload_count): 382 | return "\n".join([ 383 | f' ' 384 | for module in self.modules[:preload_count] 385 | ]) 386 | 387 | def export_index(self, out_path, benchmarks): 388 | with open(out_path / 'index.html', 'w') as f: 389 | f.write( 390 | self.template('index').substitute( 391 | dict(info=self.output_info(), 392 | benchmarks=self.output_benchmark_list(out_path, 393 | benchmarks)))) 394 | 395 | def output_benchmark_list(self, out_path, benchmarks): 396 | return "\n".join([ 397 | f'
  • {path.name}
  • ' 398 | for path in benchmarks 399 | ]) 400 | 401 | 402 | #============================================================================= 403 | if __name__ == "__main__": 404 | options = Options().options 405 | benchmark = Benchmark(options) 406 | benchmark.export(Path('out')) 407 | --------------------------------------------------------------------------------