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 |