Improve the development experience in debug=assets mode by reducing the number of requests to the server. We are adapting the solution used for the JS files to the CSS files. This solution consists of no longer sending all the files separately, but sending only the bundles associated with their sourcemap. This allows us to keep the same debugging experience while drastically reducing the number of requests to the server. Benchmark: saas 14.2 917 requests domcontentloaded after 3.76s master (bundling du js) 299 requests domcontentloaded after 2.03s branch (bundling js+css) 36 requests domcontentloaded after 1.01s Task id : 2463840 closes odoo/odoo#66169 Related: odoo/design-themes#453 Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
180 lines
7.0 KiB
Python
180 lines
7.0 KiB
Python
from functools import lru_cache
|
|
import json
|
|
|
|
|
|
class SourceMapGenerator:
|
|
"""
|
|
The SourceMapGenerator creates the sourcemap maps the asset bundle to the js/css files.
|
|
|
|
What is a sourcemap ? (https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map)
|
|
In brief: a source map is what makes possible to debug your processed/compiled/minified code as if you were
|
|
debugging the original, non-altered source code. It is a file that provides a mapping original <=> processed for
|
|
the browser to read.
|
|
|
|
This implementation of the SourceMapGenerator is a translation and adaptation of this implementation
|
|
in js https://github.com/mozilla/source-map. For performance purposes, we have removed all unnecessary
|
|
functions/steps for our use case. This simpler version does a line by line mapping, with the ability to
|
|
add offsets at the start and end of a file. (when we have to add comments on top a transpiled file by example).
|
|
"""
|
|
def __init__(self, source_root=None):
|
|
self._file = None
|
|
self._source_root = source_root
|
|
self._sources = {}
|
|
self._mappings = []
|
|
self._sources_contents = {}
|
|
self._version = 3
|
|
self._cache = {}
|
|
|
|
def _serialize_mappings(self):
|
|
"""
|
|
A source map mapping is encoded with the base 64 VLQ format.
|
|
This function encodes the readable source to the format.
|
|
|
|
:return the encoded content
|
|
"""
|
|
previous_generated_line = 1
|
|
previous_original_line = 0
|
|
previous_source = 0
|
|
encoded_column = base64vlq_encode(0)
|
|
result = ""
|
|
for mapping in self._mappings:
|
|
if mapping["generatedLine"] != previous_generated_line:
|
|
while mapping["generatedLine"] > previous_generated_line:
|
|
result += ";"
|
|
previous_generated_line += 1
|
|
|
|
if mapping["source"] is not None:
|
|
sourceIdx = self._sources[mapping["source"]]
|
|
source = sourceIdx - previous_source
|
|
previous_source = sourceIdx
|
|
|
|
# lines are stored 0-based in SourceMap spec version 3
|
|
line = mapping["originalLine"] - 1 - previous_original_line
|
|
previous_original_line = mapping["originalLine"] - 1
|
|
|
|
if (source, line) not in self._cache:
|
|
self._cache[(source, line)] = "".join([
|
|
encoded_column,
|
|
base64vlq_encode(source),
|
|
base64vlq_encode(line),
|
|
encoded_column,
|
|
])
|
|
|
|
result += self._cache[source, line]
|
|
return result
|
|
|
|
def to_json(self):
|
|
"""
|
|
Generates the json sourcemap.
|
|
It is the main function that assembles all the pieces.
|
|
|
|
:return {str} valid sourcemap in json format
|
|
"""
|
|
mapping = {
|
|
"version": self._version,
|
|
"sources": list(self._sources.keys()),
|
|
"mappings": self._serialize_mappings(),
|
|
"sourcesContent": [self._sources_contents[source] for source in self._sources]
|
|
}
|
|
if self._file:
|
|
mapping["file"] = self._file
|
|
|
|
if self._source_root:
|
|
mapping["sourceRoot"] = self._source_root
|
|
|
|
return mapping
|
|
|
|
def get_content(self):
|
|
"""Generates the content of the sourcemap.
|
|
|
|
:return the content of the sourcemap as a string encoded in UTF-8.
|
|
"""
|
|
# Store with XSSI-prevention prefix
|
|
return b")]}'\n" + json.dumps(self.to_json()).encode('utf8')
|
|
|
|
def add_source(self, source_name, source_content, last_index, start_offset=0):
|
|
"""Adds a new source file in the sourcemap. All the lines of the source file will be mapped line by line
|
|
to the generated file from the (last_index + start_offset). All lines between
|
|
last_index and (last_index + start_offset) will
|
|
be mapped to line 1 of the source file.
|
|
|
|
Example:
|
|
ls 1 = Line 1 from new source file
|
|
lg 1 = Line 1 from genereted file
|
|
ls 1 <=> lg 1 Line 1 from new source file is map to Line 1 from genereted file
|
|
nb_ls = number of lines in the new source file
|
|
|
|
Step 1:
|
|
ls 1 <=> lg last_index + 1
|
|
|
|
Step 2:
|
|
ls 1 <=> lg last_index + start_offset + 1
|
|
ls 2 <=> lg last_index + start_offset + 2
|
|
...
|
|
ls nb_ls <=> lg last_index + start_offset + nb_ls
|
|
|
|
|
|
:param source_name: name of the source to add
|
|
:param source_content: content of the source to add
|
|
:param last_index: Line where we start to map the new source
|
|
:param start_offset: Number of lines to pass in the generated file before starting mapping line by line
|
|
"""
|
|
source_line_count = len(source_content.split("\n"))
|
|
|
|
self._sources.setdefault(source_name, len(self._sources))
|
|
|
|
self._sources_contents[source_name] = source_content
|
|
if start_offset > 0:
|
|
# adds a mapping between the first line of the source
|
|
# and the first line of the corresponding code in the generated file.
|
|
self._mappings.append({
|
|
"generatedLine": last_index + 1,
|
|
"originalLine": 1,
|
|
"source": source_name,
|
|
})
|
|
for i in range(1, source_line_count + 1):
|
|
self._mappings.append({
|
|
"generatedLine": last_index + i + start_offset,
|
|
"originalLine": i,
|
|
"source": source_name,
|
|
})
|
|
|
|
|
|
B64CHARS = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
SHIFTSIZE, FLAG, MASK = 5, 1 << 5, (1 << 5) - 1
|
|
|
|
|
|
@lru_cache(maxsize=64)
|
|
def base64vlq_encode(*values):
|
|
"""
|
|
Encode Base64 VLQ encoded sequences
|
|
https://gist.github.com/mjpieters/86b0d152bb51d5f5979346d11005588b
|
|
Base64 VLQ is used in source maps.
|
|
VLQ values consist of 6 bits (matching the 64 characters of the Base64
|
|
alphabet), with the most significant bit a *continuation* flag. If the
|
|
flag is set, then the next character in the input is part of the same
|
|
integer value. Multiple VLQ character sequences so form an unbounded
|
|
integer value, in little-endian order.
|
|
The *first* VLQ value consists of a continuation flag, 4 bits for the
|
|
value, and the last bit the *sign* of the integer:
|
|
+-----+-----+-----+-----+-----+-----+
|
|
| c | b3 | b2 | b1 | b0 | s |
|
|
+-----+-----+-----+-----+-----+-----+
|
|
while subsequent VLQ characters contain 5 bits of value:
|
|
+-----+-----+-----+-----+-----+-----+
|
|
| c | b4 | b3 | b2 | b1 | b0 |
|
|
+-----+-----+-----+-----+-----+-----+
|
|
For source maps, Base64 VLQ sequences can contain 1, 4 or 5 elements.
|
|
"""
|
|
results = []
|
|
add = results.append
|
|
for v in values:
|
|
# add sign bit
|
|
v = (abs(v) << 1) | int(v < 0)
|
|
while True:
|
|
toencode, v = v & MASK, v >> SHIFTSIZE
|
|
add(toencode | (v and FLAG))
|
|
if not v:
|
|
break
|
|
return bytes(map(B64CHARS.__getitem__, results)).decode()
|