Files
odoo_source/odoo/tools/sourcemap_generator.py
Francois (fge) 4627c224ef [IMP] base: add sourcemap support for CSS files.
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>
2021-02-18 08:51:02 +00:00

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()