mach: Improve test-speedometer error reporting (#38887)

Currently speedometer results are parsed by parsing the console output
from stdout (or a file in the case of ohos). Currently json decode
errors just cause mach to crash. Instead print an error message, point
to the problematic location and exit.
A crash can happen if something else also prints, e.g. on macos, we have
the `xx threads are still running` message at shutdown. Hence this PR
doesn't really fix the unreliable nature of the current implementation,
but at least adds a helpful error message, which would point people in
the right direction.

Testing: test-speedometer is run in CI

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Jonathan Schwender 2025-08-23 13:36:57 +02:00 committed by GitHub
parent 0e38538bf2
commit 0a8146143a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -113,6 +113,22 @@ def format_with_rustfmt(check_only: bool = True) -> int:
)
def pretty_print_json_decode_error(error: json.decoder.JSONDecodeError) -> None:
print(f"json decode error: {error}")
# Print section around that character that raised the json decode error.
snippet_radius = 25
start, stop = max(0, error.pos - snippet_radius), error.pos + snippet_radius
snippet = error.doc[start:stop]
print("```")
prefix = "... " if start != 0 else ""
suffix = " ..." if stop < len(error.doc) else ""
print(prefix, snippet, suffix, sep="")
# If the snippet is multiline, this won't work as expected, but it's a best effort.
right_justification = snippet_radius + 1 + len(suffix)
print("^ Offending character".rjust(right_justification))
print("```")
@CommandProvider
class MachCommands(CommandBase):
DEFAULT_RENDER_MODE = "cpu"
@ -617,8 +633,7 @@ class MachCommands(CommandBase):
json.dump(output, f, indent=4)
def speedometer_runner(self, binary: str, bmf_output: str | None) -> None:
speedometer = json.loads(
subprocess.check_output(
output = subprocess.check_output(
[
binary,
"https://servospeedometer.netlify.app?headless=1",
@ -627,9 +642,16 @@ class MachCommands(CommandBase):
"--window-size=1100x900",
"--headless",
],
encoding="utf-8",
timeout=120,
).decode()
)
try:
speedometer = json.loads(output)
except json.decoder.JSONDecodeError as e:
pretty_print_json_decode_error(e)
print("Error: Failed to parse speedometer results")
print("This can happen if other log messages are printed while running servo...")
exit(1)
print(f"Score: {speedometer['Score']['mean']} ± {speedometer['Score']['delta']}")
@ -646,13 +668,13 @@ class MachCommands(CommandBase):
hdc_path = path.join(ohos_sdk_native, "../", "toolchains", "hdc")
def read_log_file(hdc_path: str) -> str:
subprocess.call([hdc_path, "file", "recv", log_path])
subprocess.call([hdc_path, "file", "recv", log_path, "servo.log"])
file = ""
try:
file = open("servo.log")
with open("servo.log") as file:
return file.read()
except OSError:
return ""
return file.read()
subprocess.call([hdc_path, "shell", "aa", "force-stop", "org.servo.servo"])
@ -681,9 +703,6 @@ class MachCommands(CommandBase):
# A current (2025-06-23) run took 3m 49s = 229s. We keep a safety margin
# but we will exit earlier if we see "{"
# Currently ohos has a bug where the event loop gets stuck. We produce a
# touch event every minute to prevent this
# See https://github.com/servo/servo/issues/37727
whole_file: str = ""
for i in range(10):
sleep(30)
@ -694,9 +713,21 @@ class MachCommands(CommandBase):
sleep(2)
whole_file = read_log_file(hdc_path)
break
else:
print("Error failed to find console logs in log file")
print(f"log-file contents: `{whole_file}`")
exit(1)
start_index: int = whole_file.index("[INFO script::dom::console]") + len("[INFO script::dom::console]") + 1
json_string = whole_file[start_index:]
try:
speedometer = json.loads(json_string)
except json.decoder.JSONDecodeError as e:
print(f"Error: Failed to convert log output to JSON: {e}")
pretty_print_json_decode_error(e)
print("Error: Failed to parse speedometer results")
print("This can happen if other log messages are printed while running servo...")
exit(1)
print(f"Score: {speedometer['Score']['mean']} ± {speedometer['Score']['delta']}")
if bmf_output:
self.speedometer_to_bmf(speedometer, bmf_output, profile)