Merge lp:~sidnei/lazr-js/better-module-config-compression into lp:lazr-js

Proposed by Sidnei da Silva on 2010-03-16
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sidnei/lazr-js/better-module-config-compression
Merge into: lp:lazr-js
Prerequisite: lp:~sidnei/lazr-js/newer-buildout
Diff against target: 301 lines (+128/-72)
2 files modified
src-py/lazr/js/meta.py (+59/-5)
src-py/lazr/js/tests/test_meta.py (+69/-67)
To merge this branch: bzr merge lp:~sidnei/lazr-js/better-module-config-compression
Reviewer Review Type Date Requested Status
Māris Fogels (community) 2010-03-16 Approve on 2010-03-16
Graham Binns (community) 2010-03-16 Abstain on 2010-03-16
Review via email: mp+21427@code.launchpad.net

Description of the Change

- If a string literal is used more than once in requires/use/after,
  declare a constant inside a closure so that minification can
  compress the module config better.

To post a comment you must log in.
Graham Binns (gmb) wrote :

I'm requesting Maris's review on this because I don't have the necessary knowledge to review it.

review: Abstain
Māris Fogels (mars) wrote :
Download full text (3.5 KiB)

Hi Sidnei,

This change's functionality looks good. I asked for a few more source code comments in key areas, and had one suggestion about the testing. With the addition source comments, I think you can land it.

I think the code for counting and generating the string literal optimizations is very close to needing a refactoring. If the optimizer was a collection of methods or functions then it would be a lot easier to test their behaviour in isolation. The testing appears to be based on the inputs and outputs of a large piece of sample data because all of the functionality is in a linear script right now, and this makes the tests very difficult to read (and therefore, difficult for someone else to debug). I do not think that this testing approach can be taken much farther beyond what you have now.

Maris

=== modified file 'src-py/lazr/js/meta.py'
--- src-py/lazr/js/meta.py 2010-01-27 21:20:17 +0000
+++ src-py/lazr/js/meta.py 2010-03-16 04:42:14 +0000

@@ -101,17 +105,48 @@
                 entry["ext"] = self.ext
                 if self.include_skin and entry.get("skinnable"):
                     self.generate_skin_modules(entry, metadata, root)
- else:
- entry['skinnable'] = False
             metadata.extend(meta)

         modules = {}
+ all_literals = []
         for meta in metadata:
             name = meta.pop("name")
             modules[name] = meta
-
- open(out, "w").write("var %s = %s;" %
- (var_name, simplejson.dumps(modules)))
+ all_literals.append(name)
+ all_literals.extend(meta.get("use", ()))
+ all_literals.extend(meta.get("requires", ()))
+ all_literals.extend(meta.get("after", ()))
+
+ # Only optimize string literals if they are used more than
+ # twice, since otherwise the optimization is pointless.
+ literals = [k for k, g in itertools.groupby(sorted(all_literals))
+ if len(list(g)) > 1]
+ literals.extend(["css", "js"])

So I assume the loop is an easy way to count the literal occurances. Why do
you manually add the css and js literals? Please add a comment explaining
this section.

+
+ # For each string literal we are interested in, generate a
+ # variable declaration for the string literal, to improve
+ # minification.
+ variables_decl = "var %s" % ",\n ".join(
+ ["%s = \"%s\"" % (NAME_RE.sub("_", literal).upper(), literal)
+ for literal in sorted(literals)])

So this creates a string like "var FOO='foo',BAR='bar'..."?
It looks like you are correcting the literal names? Please add a comment
demonstrating what this code outputs.

+
+ def literal_sub(match):
+ if match.group(2) in literals:
+ return (match.group(1) +
+ NAME_RE.sub("_", match.group(2)).upper() +
+ match.group(3))
+ return match.group(0)

I am not sure what this does. Why are you replacing underscores here? Was that
not done in the last code block? Please add a comment explaining this section.

+
+ modules_decl = LITERAL_RE.sub(l...

Read more...

review: Approve
175. By Sidnei da Silva on 2010-03-16

Merged newer-buildout into better-module-config-compression.

176. By Sidnei da Silva on 2010-03-16

- Add some more comments as recommended by Maris, simplify part of the code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src-py/lazr/js/meta.py'
2--- src-py/lazr/js/meta.py 2010-01-27 21:20:17 +0000
3+++ src-py/lazr/js/meta.py 2010-03-16 19:10:44 +0000
4@@ -2,6 +2,7 @@
5 import re
6 import sys
7 import glob
8+import itertools
9 import optparse
10
11 import simplejson
12@@ -15,6 +16,9 @@
13 "({[^{}a-zA-Z]*(use|requires|optional|after|"
14 "supersedes|omit|skinnable)[\s\'\":]*[^{}]*})\s*\);", re.M | re.S)
15
16+LITERAL_RE = re.compile("([\[ ]+)\"([\w\.\+-]+)\"([^:])")
17+NAME_RE = re.compile("[\.\+-]")
18+
19
20 def extract_metadata(src):
21 """Extract metadata about an YUI module, given it's source."""
22@@ -101,17 +105,67 @@
23 entry["ext"] = self.ext
24 if self.include_skin and entry.get("skinnable"):
25 self.generate_skin_modules(entry, metadata, root)
26- else:
27- entry['skinnable'] = False
28 metadata.extend(meta)
29
30 modules = {}
31+ all_literals = []
32 for meta in metadata:
33 name = meta.pop("name")
34 modules[name] = meta
35-
36- open(out, "w").write("var %s = %s;" %
37- (var_name, simplejson.dumps(modules)))
38+ all_literals.append(name)
39+ all_literals.extend(meta.get("use", ()))
40+ all_literals.extend(meta.get("requires", ()))
41+ all_literals.extend(meta.get("after", ()))
42+ all_literals.append(meta["type"])
43+
44+ # Only optimize string literals if they are used more than
45+ # once, since otherwise the optimization is pointless. This
46+ # loop here is basically filtering out those that only occur
47+ # once.
48+ literals = [k for k, g in itertools.groupby(sorted(all_literals))
49+ if len(list(g)) > 1]
50+
51+ # For each string literal we are interested in, generate a
52+ # variable declaration for the string literal, to improve
53+ # minification.
54+ #
55+ # The variable name is generated by replacing [".", "+", "-"]
56+ # with an underscore and then make that the variable name,
57+ # uppercase.
58+ #
59+ # We'll save a mapping of literal -> variable name here for
60+ # reuse below on the re.sub() helper function.
61+ literals_map = dict([(literal, NAME_RE.sub("_", literal).upper())
62+ for literal in literals])
63+ variables_decl = "var %s" % ",\n ".join(
64+ ["%s = \"%s\"" % (variable, literal)
65+ for literal, variable in sorted(literals_map.iteritems())])
66+
67+ # This re.sub() helper function operates on the JSON-ified
68+ # representation of the modules, by looking for string
69+ # literals that occur over the JSON structure but *not* as
70+ # attribute names.
71+ #
72+ # If a string literal is found that matches the list of
73+ # literals we have declared as variables above, then replace
74+ # the it by the equivalent variable, otherwise return the
75+ # original string.
76+ def literal_sub(match):
77+ literal = match.group(2)
78+ if literal in literals_map:
79+ return match.group(1) + literals_map[literal] + match.group(3)
80+ return match.group(0)
81+
82+ modules_decl = LITERAL_RE.sub(literal_sub, simplejson.dumps(modules))
83+
84+ module_config = open(out, "w")
85+ try:
86+ module_config.write("""var %s = (function(){
87+ %s;
88+ return %s;
89+})();""" % (var_name, variables_decl, modules_decl))
90+ finally:
91+ module_config.close()
92
93 def generate_skin_modules(self, entry, metadata, root):
94 # Generate a skin module definition, since YUI assumes that
95
96=== modified file 'src-py/lazr/js/tests/test_meta.py'
97--- src-py/lazr/js/tests/test_meta.py 2010-01-27 14:01:09 +0000
98+++ src-py/lazr/js/tests/test_meta.py 2010-03-16 19:10:44 +0000
99@@ -207,7 +207,7 @@
100 """
101 root = self.makeDir()
102 anim = self.makeDir(path=os.path.join(root, "anim"))
103- sample = self.makeFile("""\
104+ self.makeFile("""\
105 YUI.add('lazr.anim', function(Y){
106 Y.log('Hello World');
107 }, "0.1", {"use": ["dom"]});
108@@ -218,20 +218,15 @@
109 output=output, exclude_regex="")
110 builder.do_build()
111
112- module = {}
113- config = {"lazr.anim":
114- {"path": "anim/anim-min.js",
115- "use": ["dom"],
116- "ext": True,
117- "type": "js",
118- "skinnable": False
119- }
120- }
121-
122 got = open(output, "r").read()
123 prefix = got[:18]
124+ modules = got.splitlines()[-2].strip()[7:-1]
125 self.assertEquals(prefix, "var LAZR_CONFIG = ")
126- self.assertEquals(simplejson.loads(got[18:-1]), config)
127+ self.assertTrue('"path": "anim/anim-min.js"' in modules)
128+ self.assertFalse('"skinnable": false' in modules)
129+ self.assertTrue('"type": "js"' in modules)
130+ self.assertTrue('"use": ["dom"]' in modules)
131+ self.assertTrue('"ext": true' in modules)
132
133 def test_extract_skinnable(self):
134 """
135@@ -242,14 +237,14 @@
136 """
137 root = self.makeDir()
138 anim = self.makeDir(path=os.path.join(root, "anim"))
139- sample = self.makeFile("""\
140+ self.makeFile("""\
141 YUI.add('lazr.anim', function(Y){
142 Y.log('Hello World');
143 }, "0.1", {"use": ["dom"], "requires": ["widget"], "skinnable": true});
144 """, basename="anim.js", dirname=anim)
145
146 skin = self.makeDir(path=os.path.join(anim, "assets", "skins", "sam"))
147- css = self.makeFile("""\
148+ self.makeFile("""\
149 /* anim-skin.css */
150 """, basename="anim-skin.css", dirname=skin)
151
152@@ -259,29 +254,24 @@
153 prefix="lazr")
154 builder.do_build()
155
156- config = {
157- "skin-sam-lazr.anim":
158- {"path": "lazr/anim/assets/skins/sam/anim-skin.css",
159- "type": "css",
160- "ext": True,
161- "after": ["cssreset", "cssfonts", "cssgrids",
162- "cssreset-context", "cssfonts-context",
163- "cssgrids-context", "skin-sam-widget"],
164- },
165- "lazr.anim":
166- {"path": "lazr/anim/anim-min.js",
167- "use": ["dom"],
168- "requires": ["widget"],
169- "type": "js",
170- "ext": True,
171- "skinnable": True,
172- }
173- }
174-
175 got = open(output, "r").read()
176 prefix = got[:18]
177+ modules = got.splitlines()[-2].strip()[7:-1]
178 self.assertEquals(prefix, "var LAZR_CONFIG = ")
179- self.assertEquals(simplejson.loads(got[18:-1]), config)
180+ self.assertTrue(
181+ '"path": "lazr/anim/assets/skins/sam/anim-skin.css"' in modules)
182+ self.assertTrue(
183+ '"path": "lazr/anim/anim-min.js"' in modules)
184+ self.assertTrue('"skinnable": true' in modules)
185+ self.assertTrue('"type": "js"' in modules)
186+ self.assertTrue('"type": "css"' in modules)
187+ self.assertTrue('"use": ["dom"]' in modules)
188+ self.assertTrue('"requires": ["widget"]' in modules)
189+ self.assertTrue(
190+ ('"after": ["cssreset", "cssfonts", "cssgrids", '
191+ '"cssreset-context", "cssfonts-context", "cssgrids-context", '
192+ '"skin-sam-widget"]') in modules)
193+ self.assertTrue('"ext": true' in modules)
194
195 def test_extract_skinnable_with_lazr_conventions(self):
196 """
197@@ -294,19 +284,19 @@
198 """
199 root = self.makeDir()
200 anim = self.makeDir(path=os.path.join(root, "anim"))
201- sample = self.makeFile("""\
202+ self.makeFile("""\
203 YUI.add('lazr.anim', function(Y){
204 Y.log('Hello World');
205 }, "0.1", {"use": ["dom"], "requires": ["widget"], "skinnable": true});
206 """, basename="anim.js", dirname=anim)
207
208 assets = self.makeDir(path=os.path.join(anim, "assets"))
209- css_core = self.makeFile("""\
210+ self.makeFile("""\
211 /* purty-anim-core.css */
212 """, basename="purty-anim-core.css", dirname=assets)
213
214 skin = self.makeDir(path=os.path.join(assets, "skins", "sam"))
215- css_skin = self.makeFile("""\
216+ self.makeFile("""\
217 /* purty-anim-skin.css */
218 """, basename="purty-anim-skin.css", dirname=skin)
219
220@@ -316,39 +306,51 @@
221 prefix="lazr")
222 builder.do_build()
223
224- config = {
225- "skin-sam-lazr.anim+core":
226- {"path": "lazr/anim/assets/purty-anim-core.css",
227- "type": "css",
228- "ext": True,
229- "after": ["cssreset", "cssfonts", "cssgrids",
230- "cssreset-context", "cssfonts-context",
231- "cssgrids-context", "skin-sam-widget"],
232- },
233- "skin-sam-lazr.anim":
234- {"path": "lazr/anim/assets/skins/sam/purty-anim-skin.css",
235- "type": "css",
236- "ext": True,
237- "after": ["cssreset", "cssfonts", "cssgrids",
238- "cssreset-context", "cssfonts-context",
239- "cssgrids-context", "skin-sam-widget",
240- "skin-sam-lazr.anim+core"],
241- "requires": ["skin-sam-lazr.anim+core"],
242- },
243- "lazr.anim":
244- {"path": "lazr/anim/anim-min.js",
245- "use": ["dom"],
246- "requires": ["widget"],
247- "type": "js",
248- "ext": True,
249- "skinnable": True,
250- }
251- }
252-
253 got = open(output, "r").read()
254 prefix = got[:18]
255+ lines = got.splitlines()
256+ modules = lines[-2].strip()[7:-1]
257+ variables = {}
258+ for line in lines[1:-2]:
259+ variable, literal = line.split("=")
260+ variable = variable.strip(" ").split(" ")[-1]
261+ variables[variable] = literal.strip(" ").split("\"")[1]
262 self.assertEquals(prefix, "var LAZR_CONFIG = ")
263- self.assertEquals(simplejson.loads(got[18:-1]), config)
264+
265+ # Variables are only defined for optimizing minification if
266+ # they are used more than once, see how "dom" and "js" are not
267+ # included in this list, even though they are used by the
268+ # module. All the other names are used more than once due to
269+ # the two CSS files that are part of the skin for this module.
270+ self.assertEquals(variables, {
271+ "CSS": "css",
272+ "CSSFONTS": "cssfonts",
273+ "CSSFONTS_CONTEXT": "cssfonts-context",
274+ "CSSGRIDS": "cssgrids",
275+ "CSSGRIDS_CONTEXT": "cssgrids-context",
276+ "CSSRESET": "cssreset",
277+ "CSSRESET_CONTEXT": "cssreset-context",
278+ "SKIN_SAM_LAZR_ANIM_CORE": "skin-sam-lazr.anim+core",
279+ "SKIN_SAM_WIDGET": "skin-sam-widget"})
280+
281+ self.assertTrue(
282+ '"path": "lazr/anim/assets/purty-anim-core.css"' in modules)
283+ self.assertTrue(
284+ '"path": "lazr/anim/assets/skins/sam/purty-anim-skin.css"'
285+ in modules)
286+ self.assertTrue(
287+ '"path": "lazr/anim/anim-min.js"' in modules)
288+ self.assertTrue('"skinnable": true' in modules)
289+ self.assertTrue('"type": "js"' in modules)
290+ self.assertTrue('"type": CSS' in modules)
291+ self.assertTrue('"use": ["dom"]' in modules)
292+ self.assertTrue('"requires": ["widget"]' in modules)
293+ self.assertTrue('"requires": [SKIN_SAM_LAZR_ANIM_CORE]' in modules)
294+ self.assertTrue(
295+ ('"after": [CSSRESET, CSSFONTS, CSSGRIDS, '
296+ 'CSSRESET_CONTEXT, CSSFONTS_CONTEXT, CSSGRIDS_CONTEXT, '
297+ 'SKIN_SAM_WIDGET, SKIN_SAM_LAZR_ANIM_CORE]') in modules)
298+ self.assertTrue('"ext": true' in modules)
299
300
301 def test_suite():

Subscribers

People subscribed via source and target branches