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 @CommandProvider
class MachCommands(CommandBase): class MachCommands(CommandBase):
DEFAULT_RENDER_MODE = "cpu" DEFAULT_RENDER_MODE = "cpu"
@ -617,19 +633,25 @@ class MachCommands(CommandBase):
json.dump(output, f, indent=4) json.dump(output, f, indent=4)
def speedometer_runner(self, binary: str, bmf_output: str | None) -> None: def speedometer_runner(self, binary: str, bmf_output: str | None) -> None:
speedometer = json.loads( output = subprocess.check_output(
subprocess.check_output( [
[ binary,
binary, "https://servospeedometer.netlify.app?headless=1",
"https://servospeedometer.netlify.app?headless=1", "--pref",
"--pref", "dom_allow_scripts_to_close_windows",
"dom_allow_scripts_to_close_windows", "--window-size=1100x900",
"--window-size=1100x900", "--headless",
"--headless", ],
], encoding="utf-8",
timeout=120, 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']}") 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") hdc_path = path.join(ohos_sdk_native, "../", "toolchains", "hdc")
def read_log_file(hdc_path: str) -> str: 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 = "" file = ""
try: try:
file = open("servo.log") with open("servo.log") as file:
return file.read()
except OSError: except OSError:
return "" return ""
return file.read()
subprocess.call([hdc_path, "shell", "aa", "force-stop", "org.servo.servo"]) 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 # A current (2025-06-23) run took 3m 49s = 229s. We keep a safety margin
# but we will exit earlier if we see "{" # 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 = "" whole_file: str = ""
for i in range(10): for i in range(10):
sleep(30) sleep(30)
@ -694,9 +713,21 @@ class MachCommands(CommandBase):
sleep(2) sleep(2)
whole_file = read_log_file(hdc_path) whole_file = read_log_file(hdc_path)
break 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 start_index: int = whole_file.index("[INFO script::dom::console]") + len("[INFO script::dom::console]") + 1
json_string = whole_file[start_index:] json_string = whole_file[start_index:]
speedometer = json.loads(json_string) 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']}") print(f"Score: {speedometer['Score']['mean']} ± {speedometer['Score']['delta']}")
if bmf_output: if bmf_output:
self.speedometer_to_bmf(speedometer, bmf_output, profile) self.speedometer_to_bmf(speedometer, bmf_output, profile)