From dae36d4381352cd16aa6c556d11e32dd959de94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:15:12 +0100 Subject: [PATCH 1/8] Add migration to convert duration to milliseconds --- ...c3d4e5f6_duration_millisecond_precision.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 web/server/codechecker_server/migrations/report/versions/a1b2c3d4e5f6_duration_millisecond_precision.py 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..f0d1add0ed --- /dev/null +++ b/web/server/codechecker_server/migrations/report/versions/a1b2c3d4e5f6_duration_millisecond_precision.py @@ -0,0 +1,52 @@ +""" +Duration millisecond precision + +Revision ID: a1b2c3d4e5f6 +Revises: 198654dac219 +Create Date: 2026-02-24 13:14:00.000000 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = 'a1b2c3d4e5f6' +down_revision = '198654dac219' +branch_labels = None +depends_on = None + + +def upgrade(): + connection = op.get_bind() + dialect = connection.dialect.name + + if dialect == 'postgresql': + op.execute(""" + UPDATE runs + SET duration = duration * 1000 + WHERE duration != -1 + """) + elif dialect == 'sqlite': + op.execute(""" + UPDATE runs + SET duration = duration * 1000 + WHERE duration != -1 + """) + + +def downgrade(): + connection = op.get_bind() + dialect = connection.dialect.name + + if dialect == 'postgresql': + op.execute(""" + UPDATE runs + SET duration = duration / 1000 + WHERE duration != -1 + """) + elif dialect == 'sqlite': + op.execute(""" + UPDATE runs + SET duration = duration / 1000 + WHERE duration != -1 + """) From 438699ff343c4f3254c391f11ed712a443888443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:15:22 +0100 Subject: [PATCH 2/8] Store duration in milliseconds in database model --- web/server/codechecker_server/database/run_db_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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): From 8a7d3d821f0dd96d0142fcbc0faea4337ec243e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:15:39 +0100 Subject: [PATCH 3/8] Accumulate duration in milliseconds from metadata --- web/server/codechecker_server/api/mass_store_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From fc7c6b959369f906d4ddd4222c02e38efc8e87b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:15:48 +0100 Subject: [PATCH 4/8] Display duration with millisecond precision in CLI --- web/client/codechecker_client/cmd_line_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = [] From f4a539ed1e06d347534c6f709a30c60a89eb4940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:15:59 +0100 Subject: [PATCH 5/8] Still display duration as seconds in web UI --- web/server/vue-cli/src/views/RunList.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; From d8e1a76a639ead6a1121c2bebd6871ade308fbca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 13:17:29 +0100 Subject: [PATCH 6/8] Document duration unit in API as milliseconds --- web/api/report_server.thrift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 3a189ab5ccf4b3a3fdd5a28666999b99358282fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 14:23:35 +0100 Subject: [PATCH 7/8] Add test for millisecond precision in CLI output --- web/client/tests/unit/test_cmd_line_client.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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) From e5a77a8c733d8c7fbcd0138805477d2d360699c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20F=C3=BCl=C3=B6p?= Date: Tue, 24 Feb 2026 15:47:55 +0100 Subject: [PATCH 8/8] Simplify migration by removing dialect branching --- ...c3d4e5f6_duration_millisecond_precision.py | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) 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 index f0d1add0ed..0b02ad7e0d 100644 --- 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 @@ -7,7 +7,6 @@ """ from alembic import op -import sqlalchemy as sa revision = 'a1b2c3d4e5f6' @@ -17,36 +16,16 @@ def upgrade(): - connection = op.get_bind() - dialect = connection.dialect.name - - if dialect == 'postgresql': - op.execute(""" - UPDATE runs - SET duration = duration * 1000 - WHERE duration != -1 - """) - elif dialect == 'sqlite': - op.execute(""" - UPDATE runs - SET duration = duration * 1000 - WHERE duration != -1 - """) + op.execute(""" + UPDATE runs + SET duration = duration * 1000 + WHERE duration != -1 + """) def downgrade(): - connection = op.get_bind() - dialect = connection.dialect.name - - if dialect == 'postgresql': - op.execute(""" - UPDATE runs - SET duration = duration / 1000 - WHERE duration != -1 - """) - elif dialect == 'sqlite': - op.execute(""" - UPDATE runs - SET duration = duration / 1000 - WHERE duration != -1 - """) + op.execute(""" + UPDATE runs + SET duration = duration / 1000 + WHERE duration != -1 + """)