Merge remote-tracking branch 'origin/develop' into breaking

This commit is contained in:
chriseth
2022-02-17 09:42:15 +01:00
99 changed files with 2494 additions and 230 deletions
+46
View File
@@ -0,0 +1,46 @@
function base64DecToArr (sBase64) {
/*\
|*|
|*| Base64 / binary data / UTF-8 strings utilities
|*|
|*| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
|*|
\*/
/* Array of bytes to Base64 string decoding */
function b64ToUint6 (nChr) {
return nChr > 64 && nChr < 91 ?
nChr - 65
: nChr > 96 && nChr < 123 ?
nChr - 71
: nChr > 47 && nChr < 58 ?
nChr + 4
: nChr === 43 ?
62
: nChr === 47 ?
63
:
0;
}
var
nInLen = sBase64.length,
nOutLen = nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sBase64.charCodeAt(nInIdx)) << 6 * (3 - nMod4);
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
}
nUint24 = 0;
}
}
return taBytes;
}
+4 -2
View File
@@ -40,6 +40,8 @@ else
BUILD_DIR="$1"
fi
apt-get update && apt-get install lz4
WORKSPACE=/root/project
cd $WORKSPACE
@@ -71,8 +73,8 @@ make soljson
cd ..
mkdir -p upload
cp "$BUILD_DIR/libsolc/soljson.js" upload/
cp "$BUILD_DIR/libsolc/soljson.js" ./
scripts/ci/pack_soljson.sh "$BUILD_DIR/libsolc/soljson.js" "$BUILD_DIR/libsolc/soljson.wasm" upload/soljson.js
cp upload/soljson.js ./
OUTPUT_SIZE=$(ls -la soljson.js)
+116
View File
@@ -0,0 +1,116 @@
function uncompress(source, uncompressedSize) {
/*
based off https://github.com/emscripten-core/emscripten/blob/main/third_party/mini-lz4.js
The license only applies to the body of this function (``uncompress``).
====
MiniLZ4: Minimal LZ4 block decoding and encoding.
based off of node-lz4, https://github.com/pierrec/node-lz4
====
Copyright (c) 2012 Pierre Curto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
====
changes have the same license
*/
/**
* Decode a block. Assumptions: input contains all sequences of a
* chunk, output is large enough to receive the decoded data.
* If the output buffer is too small, an error will be thrown.
* If the returned value is negative, an error occurred at the returned offset.
*
* @param {ArrayBufferView} input input data
* @param {ArrayBufferView} output output data
* @param {number=} sIdx
* @param {number=} eIdx
* @return {number} number of decoded bytes
* @private
*/
function uncompressBlock (input, output, sIdx, eIdx) {
sIdx = sIdx || 0
eIdx = eIdx || (input.length - sIdx)
// Process each sequence in the incoming data
for (var i = sIdx, n = eIdx, j = 0; i < n;) {
var token = input[i++]
// Literals
var literals_length = (token >> 4)
if (literals_length > 0) {
// length of literals
var l = literals_length + 240
while (l === 255) {
l = input[i++]
literals_length += l
}
// Copy the literals
var end = i + literals_length
while (i < end) output[j++] = input[i++]
// End of buffer?
if (i === n) return j
}
// Match copy
// 2 bytes offset (little endian)
var offset = input[i++] | (input[i++] << 8)
// XXX 0 is an invalid offset value
if (offset === 0) return j
if (offset > j) return -(i-2)
// length of match copy
var match_length = (token & 0xf)
var l = match_length + 240
while (l === 255) {
l = input[i++]
match_length += l
}
// Copy the match
var pos = j - offset // position of the match copy in the current output
var end = j + match_length + 4 // minmatch = 4
while (j < end) output[j++] = output[pos++]
}
return j
}
var result = new ArrayBuffer(uncompressedSize);
var sourceIndex = 0;
var destIndex = 0;
var blockSize;
while((blockSize = (source[sourceIndex] | (source[sourceIndex + 1] << 8) | (source[sourceIndex + 2] << 16) | (source[sourceIndex + 3] << 24))) > 0)
{
sourceIndex += 4;
if (blockSize & 0x80000000)
{
blockSize &= 0x7FFFFFFFF;
for (var i = 0; i < blockSize; i++) {
result[destIndex++] = source[sourceIndex++];
}
}
else
{
destIndex += uncompressBlock(source, new Uint8Array(result, destIndex, uncompressedSize - destIndex), sourceIndex, sourceIndex + blockSize);
sourceIndex += blockSize;
}
}
return new Uint8Array(result, 0, uncompressedSize);
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(realpath "$(dirname "$0")")"
soljson_js="$1"
soljson_wasm="$2"
soljson_wasm_size=$(wc -c "${soljson_wasm}" | cut -d ' ' -f 1)
output="$3"
(( $# == 3 )) || { >&2 echo "Usage: $0 soljson.js soljson.wasm packed_soljson.js"; exit 1; }
# If this changes in an emscripten update, it's probably nothing to worry about,
# but we should double-check when it happens and adjust the tail command below.
[[ $(head -c 5 "${soljson_js}") == "null;" ]] || { >&2 echo 'Expected soljson.js to start with "null;"'; exit 1; }
echo "Packing $soljson_js and $soljson_wasm to $output."
(
echo -n 'var Module = Module || {}; Module["wasmBinary"] = '
echo -n '(function(source, uncompressedSize) {'
# Note that base64DecToArr assumes no trailing equals signs.
cpp "${script_dir}/base64DecToArr.js" | grep -v "^#.*"
# Note that mini-lz4.js assumes no file header and no frame crc checksums.
cpp "${script_dir}/mini-lz4.js" | grep -v "^#.*"
echo 'return uncompress(base64DecToArr(source), uncompressedSize);})('
echo -n '"'
# We fix lz4 format settings, remove the 8 bytes file header and remove the trailing equals signs of the base64 encoding.
lz4c --no-frame-crc --best --favor-decSpeed "${soljson_wasm}" - | tail -c +8 | base64 -w 0 | sed 's/[^A-Za-z0-9\+\/]//g'
echo '",'
echo -n "${soljson_wasm_size});"
# Remove "null;" from the js wrapper.
tail -c +6 "${soljson_js}"
) > "$output"
echo "Testing $output."
echo "process.stdout.write(require('$(realpath "${output}")').wasmBinary)" | node | cmp "${soljson_wasm}" && echo "Binaries match."
# Allow the wasm binary to be garbage collected after compilation.
echo 'Module["wasmBinary"] = undefined;' >> "${output}"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Reads multiple individual benchmark reports produced by scripts from
# test/externalTests/ from standard input and creates a combined report.
#
# Usage:
# <script name>.sh < <CONCATENATED_REPORTS>
#
# CONCATENATED_REPORTS: JSON report files concatenated into a single stream (e.g. using cat).
#
# Example:
# cat reports/externalTests/benchmark-*.json | <script name>.sh
# ------------------------------------------------------------------------------
# This file is part of solidity.
#
# solidity is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# solidity is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with solidity. If not, see <http://www.gnu.org/licenses/>
#
# (c) 2021 solidity contributors.
#------------------------------------------------------------------------------
set -euo pipefail
# We expect a series of dicts of the form {"<project>": {"<preset>": {...}}}.
# Unfortunately jq's built-in `add` filter can't handle nested dicts and
# would just overwrite values sharing a project name instead of merging them.
# This is done by first grouping the dicts into an array of the form
# [
# [{"key": "<project1>", "value": {"<preset1>": {...}}}, {"key": "<project1>", "value": {"<preset2>": {...}}, ...],
# [{"key": "<project2>", "value": {"<preset1>": {...}}}, {"key": "<project2>", "value": {"<preset2>": {...}}, ...],
# ...
# ]
# and then using reduce() on each group sharing the same project name to convert it into a
# dict having preset names as keys.
jq --slurp --indent 4 --sort-keys '
map(to_entries[]) |
group_by(.key) |
map({
(.[0].key): (
reduce (.[].value | to_entries[]) as {$key, $value} (
{}; . + {
($key): $value
}
)
)
}) |
add
'
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
# coding=utf-8
from dataclasses import asdict, dataclass, field
from typing import Dict, Optional, Tuple
import json
import re
import sys
REPORT_HEADER_REGEX = re.compile(r'''
^[|\s]+ Solc[ ]version:\s*(?P<solc_version>[\w\d.]+)
[|\s]+ Optimizer[ ]enabled:\s*(?P<optimize>[\w]+)
[|\s]+ Runs:\s*(?P<runs>[\d]+)
[|\s]+ Block[ ]limit:\s*(?P<block_limit>[\d]+)\s*gas
[|\s]+$
''', re.VERBOSE)
METHOD_HEADER_REGEX = re.compile(r'^[|\s]+Methods[|\s]+$')
METHOD_COLUMN_HEADERS_REGEX = re.compile(r'''
^[|\s]+ Contract
[|\s]+ Method
[|\s]+ Min
[|\s]+ Max
[|\s]+ Avg
[|\s]+ \#[ ]calls
[|\s]+ \w+[ ]\(avg\)
[|\s]+$
''', re.VERBOSE)
METHOD_ROW_REGEX = re.compile(r'''
^[|\s]+ (?P<contract>[^|]+)
[|\s]+ (?P<method>[^|]+)
[|\s]+ (?P<min>[^|]+)
[|\s]+ (?P<max>[^|]+)
[|\s]+ (?P<avg>[^|]+)
[|\s]+ (?P<call_count>[^|]+)
[|\s]+ (?P<eur_avg>[^|]+)
[|\s]+$
''', re.VERBOSE)
FRAME_REGEX = re.compile(r'^[-|\s]+$')
DEPLOYMENT_HEADER_REGEX = re.compile(r'^[|\s]+Deployments[|\s]+% of limit[|\s]+$')
DEPLOYMENT_ROW_REGEX = re.compile(r'''
^[|\s]+ (?P<contract>[^|]+)
[|\s]+ (?P<min>[^|]+)
[|\s]+ (?P<max>[^|]+)
[|\s]+ (?P<avg>[^|]+)
[|\s]+ (?P<percent_of_limit>[^|]+)\s*%
[|\s]+ (?P<eur_avg>[^|]+)
[|\s]+$
''', re.VERBOSE)
class ReportError(Exception):
pass
class ReportValidationError(ReportError):
pass
class ReportParsingError(Exception):
def __init__(self, message: str, line: str, line_number: int):
# pylint: disable=useless-super-delegation # It's not useless, it adds type annotations.
super().__init__(message, line, line_number)
def __str__(self):
return f"Parsing error on line {self.args[2] + 1}: {self.args[0]}\n{self.args[1]}"
@dataclass(frozen=True)
class MethodGasReport:
min_gas: int
max_gas: int
avg_gas: int
call_count: int
total_gas: int = field(init=False)
def __post_init__(self):
object.__setattr__(self, 'total_gas', self.avg_gas * self.call_count)
@dataclass(frozen=True)
class ContractGasReport:
min_deployment_gas: Optional[int]
max_deployment_gas: Optional[int]
avg_deployment_gas: Optional[int]
methods: Optional[Dict[str, MethodGasReport]]
total_method_gas: int = field(init=False, default=0)
def __post_init__(self):
if self.methods is not None:
object.__setattr__(self, 'total_method_gas', sum(method.total_gas for method in self.methods.values()))
@dataclass(frozen=True)
class GasReport:
solc_version: str
optimize: bool
runs: int
block_limit: int
contracts: Dict[str, ContractGasReport]
total_method_gas: int = field(init=False)
total_deployment_gas: int = field(init=False)
def __post_init__(self):
object.__setattr__(self, 'total_method_gas', sum(
total_method_gas
for total_method_gas in (contract.total_method_gas for contract in self.contracts.values())
if total_method_gas is not None
))
object.__setattr__(self, 'total_deployment_gas', sum(
contract.avg_deployment_gas
for contract in self.contracts.values()
if contract.avg_deployment_gas is not None
))
def to_json(self):
return json.dumps(asdict(self), indent=4, sort_keys=True)
def parse_bool(input_string: str) -> bool:
if input_string == 'true':
return True
elif input_string == 'false':
return True
else:
raise ValueError(f"Invalid boolean value: '{input_string}'")
def parse_optional_int(input_string: str, default: Optional[int] = None) -> Optional[int]:
if input_string.strip() == '-':
return default
return int(input_string)
def parse_report_header(line: str) -> Optional[dict]:
match = REPORT_HEADER_REGEX.match(line)
if match is None:
return None
return {
'solc_version': match.group('solc_version'),
'optimize': parse_bool(match.group('optimize')),
'runs': int(match.group('runs')),
'block_limit': int(match.group('block_limit')),
}
def parse_method_row(line: str, line_number: int) -> Optional[Tuple[str, str, MethodGasReport]]:
match = METHOD_ROW_REGEX.match(line)
if match is None:
raise ReportParsingError("Expected a table row with method details.", line, line_number)
avg_gas = parse_optional_int(match['avg'])
call_count = int(match['call_count'])
if avg_gas is None and call_count == 0:
# No calls, no gas values. Uninteresting. Skip the row.
return None
return (
match['contract'].strip(),
match['method'].strip(),
MethodGasReport(
min_gas=parse_optional_int(match['min'], avg_gas),
max_gas=parse_optional_int(match['max'], avg_gas),
avg_gas=avg_gas,
call_count=call_count,
)
)
def parse_deployment_row(line: str, line_number: int) -> Tuple[str, int, int, int]:
match = DEPLOYMENT_ROW_REGEX.match(line)
if match is None:
raise ReportParsingError("Expected a table row with deployment details.", line, line_number)
return (
match['contract'].strip(),
parse_optional_int(match['min'].strip()),
parse_optional_int(match['max'].strip()),
int(match['avg'].strip()),
)
def preprocess_unicode_frames(input_string: str) -> str:
# The report has a mix of normal pipe chars and its unicode variant.
# Let's just replace all frame chars with normal pipes for easier parsing.
return input_string.replace('\u2502', '|').replace('·', '|')
def parse_report(rst_report: str) -> GasReport:
report_params = None
methods_by_contract = {}
deployment_costs = {}
expected_row_type = None
for line_number, line in enumerate(preprocess_unicode_frames(rst_report).splitlines()):
try:
if (
line.strip() == "" or
FRAME_REGEX.match(line) is not None or
METHOD_COLUMN_HEADERS_REGEX.match(line) is not None
):
continue
if METHOD_HEADER_REGEX.match(line) is not None:
expected_row_type = 'method'
continue
if DEPLOYMENT_HEADER_REGEX.match(line) is not None:
expected_row_type = 'deployment'
continue
new_report_params = parse_report_header(line)
if new_report_params is not None:
if report_params is not None:
raise ReportParsingError("Duplicate report header.", line, line_number)
report_params = new_report_params
continue
if expected_row_type == 'method':
parsed_row = parse_method_row(line, line_number)
if parsed_row is None:
continue
(contract, method, method_report) = parsed_row
if contract not in methods_by_contract:
methods_by_contract[contract] = {}
if method in methods_by_contract[contract]:
# Report must be generated with full signatures for method names to be unambiguous.
raise ReportParsingError(f"Duplicate method row for '{contract}.{method}'.", line, line_number)
methods_by_contract[contract][method] = method_report
elif expected_row_type == 'deployment':
(contract, min_gas, max_gas, avg_gas) = parse_deployment_row(line, line_number)
if contract in deployment_costs:
raise ReportParsingError(f"Duplicate contract deployment row for '{contract}'.", line, line_number)
deployment_costs[contract] = (min_gas, max_gas, avg_gas)
else:
assert expected_row_type is None
raise ReportParsingError("Found data row without a section header.", line, line_number)
except ValueError as error:
raise ReportParsingError(error.args[0], line, line_number) from error
if report_params is None:
raise ReportValidationError("Report header not found.")
report_params['contracts'] = {
contract: ContractGasReport(
min_deployment_gas=deployment_costs.get(contract, (None, None, None))[0],
max_deployment_gas=deployment_costs.get(contract, (None, None, None))[1],
avg_deployment_gas=deployment_costs.get(contract, (None, None, None))[2],
methods=methods_by_contract.get(contract),
)
for contract in methods_by_contract.keys() | deployment_costs.keys()
}
return GasReport(**report_params)
if __name__ == "__main__":
try:
report = parse_report(sys.stdin.read())
print(report.to_json())
except ReportError as exception:
print(f"{exception}", file=sys.stderr)
sys.exit(1)
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Reads a combined benchmark report from standard input and outputs an abbreviated
# report containing only totals. Can handle individual reports coming directly
# from scripts in test/externalTests/ as well as combined report from merge_benchmarks.sh.
#
# Usage:
# <script name>.sh < <CONCATENATED_REPORTS>
#
# CONCATENATED_REPORTS: JSON report files concatenated into a single stream (e.g. using cat).
#
# Example:
# cat reports/externalTests/benchmark-*.json | <script name>.sh
# ------------------------------------------------------------------------------
# This file is part of solidity.
#
# solidity is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# solidity is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with solidity. If not, see <http://www.gnu.org/licenses/>
#
# (c) 2021 solidity contributors.
#------------------------------------------------------------------------------
set -euo pipefail
REPO_ROOT=$(realpath "$(dirname "$0")/../..")
# Iterates over presets in a dict of the form {"<project>": {"<preset>": {...}}} and for each
# one preserves only the few keys with totals that we want to see in the summary.
exec "${REPO_ROOT}/scripts/externalTests/merge_benchmarks.sh" | jq --indent 4 --sort-keys '
with_entries({
key: .key,
value: .value | with_entries({
key: .key,
value: {
bytecode_size: .value.total_bytecode_size,
method_gas: .value.gas.total_method_gas,
deployment_gas: .value.gas.total_deployment_gas,
version: .value.project.version
}
})
})
'