diff --git a/web/api/report_server.thrift b/web/api/report_server.thrift index 94d2597838..b1ee3182fd 100644 --- a/web/api/report_server.thrift +++ b/web/api/report_server.thrift @@ -189,7 +189,7 @@ struct RunData { 1: i64 runId, // Unique id of the run. 2: string runDate, // Date of the run last updated. 3: string name, // Human-given identifier. - 4: i64 duration, // Duration of the run (-1 if not finished). + 4: i64 duration, // Duration of the run in milliseconds (-1 if not finished). 5: i64 resultCount, // Number of unresolved results (review status is not FALSE_POSITIVE or INTENTIONAL) in the run. 6: string runCmd, // The used check command. !!!DEPRECATED!!! This field will be empty so use the getCheckCommand API function to get the check command for a run. 7: map detectionStatusCount, // Number of reports with a particular detection status. diff --git a/web/client/codechecker_client/cmd_line_client.py b/web/client/codechecker_client/cmd_line_client.py index 8053472a23..082efc53f6 100644 --- a/web/client/codechecker_client/cmd_line_client.py +++ b/web/client/codechecker_client/cmd_line_client.py @@ -678,7 +678,7 @@ def handle_list_runs(args): 'Duration', 'Description', 'CodeChecker version'] rows = [] for run in runs: - duration = str(timedelta(seconds=run.duration)) \ + duration = str(timedelta(milliseconds=run.duration)) \ if run.duration > -1 else 'Not finished' analyzer_statistics = [] diff --git a/web/client/tests/unit/test_cmd_line_client.py b/web/client/tests/unit/test_cmd_line_client.py index 5be6215a94..28cdbe0c8a 100644 --- a/web/client/tests/unit/test_cmd_line_client.py +++ b/web/client/tests/unit/test_cmd_line_client.py @@ -83,3 +83,27 @@ def test_enabled_checkers_json(self, get_run_data, setup_client, init_logger): stats = run_data["analyzerStatistics"]["clangsa"] self.assertIn("enabledCheckers", stats) self.assertEqual(stats["enabledCheckers"], ["a"]) + + +class DurationMillisecondPrecisionTest(unittest.TestCase): + @patch("codechecker_client.cmd_line_client.init_logger") + @patch("codechecker_client.cmd_line_client.setup_client") + @patch("codechecker_client.cmd_line_client.get_run_data") + def test_duration_shows_milliseconds(self, get_run_data, setup_client, init_logger): + run = DummyRun(1, "test_run") + run.duration = 1234 + get_run_data.return_value = [run] + + args = Args( + product_url="dummy", + sort_type="name", + sort_order="asc", + output_format="plaintext" + ) + + buf = io.StringIO() + with redirect_stdout(buf): + cmd_line_client.handle_list_runs(args) + + output = buf.getvalue() + self.assertIn("0:00:01.234", output) diff --git a/web/server/codechecker_server/api/mass_store_run.py b/web/server/codechecker_server/api/mass_store_run.py index 64a319991d..79b6c60bdb 100644 --- a/web/server/codechecker_server/api/mass_store_run.py +++ b/web/server/codechecker_server/api/mass_store_run.py @@ -954,7 +954,7 @@ def __store_analysis_statistics( }) for mip in self.__mips.values(): - self.__duration += int(sum(mip.check_durations)) + self.__duration += int(sum(mip.check_durations) * 1000) for analyzer_type, res in mip.analyzer_statistics.items(): if "version" in res: diff --git a/web/server/codechecker_server/database/run_db_model.py b/web/server/codechecker_server/database/run_db_model.py index 4d346c9513..3a5593314d 100644 --- a/web/server/codechecker_server/database/run_db_model.py +++ b/web/server/codechecker_server/database/run_db_model.py @@ -99,7 +99,7 @@ class Run(Base): id = Column(Integer, autoincrement=True, primary_key=True) date = Column(DateTime) - duration = Column(Integer) # Seconds, -1 if unfinished. + duration = Column(Integer) # Milliseconds, -1 if unfinished. name = Column(String) version = Column(String) can_delete = Column(Boolean, nullable=False, server_default=true(), @@ -110,8 +110,10 @@ def __init__(self, name, version): self.duration = -1 def mark_finished(self): - if self.duration == -1: - self.duration = ceil((datetime.now() - self.date).total_seconds()) + if self.duration != -1: + return + runtime_ms = (datetime.now() - self.date) / timedelta(milliseconds=1) + self.duration = ceil(runtime_ms) class RunLock(Base): diff --git a/web/server/codechecker_server/migrations/report/versions/a1b2c3d4e5f6_duration_millisecond_precision.py b/web/server/codechecker_server/migrations/report/versions/a1b2c3d4e5f6_duration_millisecond_precision.py new file mode 100644 index 0000000000..0b02ad7e0d --- /dev/null +++ b/web/server/codechecker_server/migrations/report/versions/a1b2c3d4e5f6_duration_millisecond_precision.py @@ -0,0 +1,31 @@ +""" +Duration millisecond precision + +Revision ID: a1b2c3d4e5f6 +Revises: 198654dac219 +Create Date: 2026-02-24 13:14:00.000000 +""" + +from alembic import op + + +revision = 'a1b2c3d4e5f6' +down_revision = '198654dac219' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute(""" + UPDATE runs + SET duration = duration * 1000 + WHERE duration != -1 + """) + + +def downgrade(): + op.execute(""" + UPDATE runs + SET duration = duration / 1000 + WHERE duration != -1 + """) diff --git a/web/server/vue-cli/src/views/RunList.vue b/web/server/vue-cli/src/views/RunList.vue index 34969cf098..4bc3f9a8c8 100644 --- a/web/server/vue-cli/src/views/RunList.vue +++ b/web/server/vue-cli/src/views/RunList.vue @@ -519,8 +519,9 @@ export default { this.analyzerStatisticsDialog = true; }, - prettifyDuration(seconds) { - if (seconds >= 0) { + prettifyDuration(milliseconds) { + if (milliseconds >= 0) { + const seconds = Math.floor(milliseconds / 1000); const durHours = Math.floor(seconds / 3600); const durMins = Math.floor(seconds / 60) - durHours * 60; const durSecs = seconds - durMins * 60 - durHours * 3600;