Skip to content

More human readable duration strings - #72350

Merged
bbovenzi merged 4 commits into
apache:mainfrom
astronomer:dev/timestamp-subsecond-durations-191d7e
Sep 9, 2026
Merged

bbovenzi merged 4 commits into
apache:mainfrom
astronomer:dev/timestamp-subsecond-durations-191d7e

Conversation

@bbovenzi

@bbovenzi bbovenzi commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Problem:

our duration format was dd:hh:mm:ss.ms, which for short durations had a lot of excessive zeros or with many ms had floating point overflow.

Screenshot 2026-08-31 at 3 45 37 PM

Solution:
Switch to a more human readable format string that can be set per locale

Screenshot 2026-08-31 at 3 41 33 PM Screenshot 2026-08-31 at 3 41 56 PM Screenshot 2026-08-31 at 3 41 45 PM
Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Claude Opus 5

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@bbovenzi bbovenzi added this to the Airflow 3.3.2 milestone Aug 31, 2026
@boring-cyborg boring-cyborg Bot added the area:UI Related to UI/UX. For Frontend Developers. label Aug 31, 2026

@ryanahamilton ryanahamilton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read through the whole diff and ran the checks locally. The core band logic in getDurationParts is solid — I probed the boundaries hard (0.9996, 59.95, 3599.6, 86399.4, …) and the carry is correct at every one of them. I also pushed all 21 shipped locales through both Intl.NumberFormat(style: "unit") and Intl.ListFormat, narrow and long, with no throws. The move off zero-padded clock strings is a clear improvement, and driving the format from CLDR with no new translation keys is the right way to get there.

Local results, on 4b6f540:

Check Result
pnpm exec tsc --p tsconfig.app.json pass
pnpm exec eslint --quiet pass
pnpm exec vitest run 1150/1151 — the single failure, DateTimeInput.test.tsx, is an unrelated 5s timeout that passes in isolation

One thing I'd call a bug, on getRelativeTime — it prints 60 minutes ago / 24 hours ago / 12 months ago where fromNow() said an hour ago / a day ago / a year ago. Details inline. The rest are layout budgets that were tuned for the always-English "1h 2m" and are now tight in the wider locales, plus some cleanup.

Three notes that don't anchor to a changed line:

  • src/utils/index.ts:21 still re-exports getDuration / renderDuration. After this PR nothing imports them from the barrel, and useDurationFormat's own doc says components must not use the raw functions — dropping the re-export would close the escape hatch the hook exists to eliminate.
  • Backfills.tsx takes formatElapsed as a third positional argument to getColumns, where the sibling DagRuns / TaskInstances column builders take an options object. Trivial, but the three read differently now.
  • Newsfragment — there isn't one. This changes how every duration in the UI renders, so it may be worth an airflow-core/newsfragments/72350.improvement.rst; entirely your call on whether it clears the user-facing bar.

@bbovenzi
bbovenzi force-pushed the dev/timestamp-subsecond-durations-191d7e branch from 4b6f540 to da399ed Compare September 1, 2026 13:55

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks.

@ryanahamilton ryanahamilton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had one more small comment and looks like you have a merge conflict. Overall looks like a nice enhancement.

@vatsrahul1001

Copy link
Copy Markdown
Contributor

@bbovenzi can you resolve conflicts

Zero-padded clock strings buried the one number that mattered: a task
that took 83ms read as 00:00:00.083, and anything under a second
rendered as 00:00:00 in chart tooltips and Gantt axis labels, which is
indistinguishable from no time at all. Someone scanning a column of
durations wants the magnitude at a glance, and roughly three
significant digits gives it to them at every scale from milliseconds
to days.

Unit names, decimal separators and plural forms now come from CLDR
through Intl rather than being hardcoded English, so durations follow
the language the rest of the UI is already using. This needs no new
translation keys, and it reproduces Intl.DurationFormat's narrow style
without requiring it, since that API is above this package's Node
floor.

Formatting goes through a hook bound to the active language because
reading the language straight off the i18next singleton put it outside
anything React tracks: a component with no reason of its own to
subscribe to a language change kept rendering the previous locale
indefinitely.
Relative times picked their unit from the unrounded gap and then rounded
inside it, so anything just short of the next unit printed that unit's
own ceiling: "60 minutes ago" where "an hour ago" was meant, and the
same at 24 hours and 12 months. Promoting after the rounding matches
what the duration path already does a few hundred lines up.

Two layout budgets were sized when every duration was the English
"1h 2m" and are too tight now that CLDR decides the width: the bar-end
labels on the slowest-task chart reserved a fixed 64px, and the Gantt
axis derived its tick spacing from an eight-character "HH:MM:SS"
estimate. German narrow needs roughly half again as much room.

The Gantt axis also placed ticks at evenly divided raw values, which
the old truncating format hid; at this precision a seven-second span
read 0s | 1.17s | 2.33s. Snapping to the same counted units the other
duration axes use keeps short spans legible.

Rounding to two units leaves "1h 2m" covering a full minute, which is
too coarse to tell two similar runs apart in a list. The exact value now
rides along in a title on the duration columns, so the compact form
stays scannable without giving up the precise number.
The hook was the only util under src/utils that callers had to reach by
its own path, so consumers imported it differently from every other util
sitting beside it. Routing it through the barrel leaves one way in.

The Dag overview test replaces the barrel wholesale rather than patching
it, so it has to name the hook for the components under test to keep
resolving.
@bbovenzi
bbovenzi force-pushed the dev/timestamp-subsecond-durations-191d7e branch from da399ed to d9ceadd Compare September 9, 2026 18:22
@bbovenzi
bbovenzi merged commit 3152d7a into apache:main Sep 9, 2026
89 checks passed
@bbovenzi
bbovenzi deleted the dev/timestamp-subsecond-durations-191d7e branch September 9, 2026 19:42
imrichardwu pushed a commit to imrichardwu/airflow that referenced this pull request Sep 11, 2026
* UI: Show durations in a readable, localized format

Zero-padded clock strings buried the one number that mattered: a task
that took 83ms read as 00:00:00.083, and anything under a second
rendered as 00:00:00 in chart tooltips and Gantt axis labels, which is
indistinguishable from no time at all. Someone scanning a column of
durations wants the magnitude at a glance, and roughly three
significant digits gives it to them at every scale from milliseconds
to days.

Unit names, decimal separators and plural forms now come from CLDR
through Intl rather than being hardcoded English, so durations follow
the language the rest of the UI is already using. This needs no new
translation keys, and it reproduces Intl.DurationFormat's narrow style
without requiring it, since that API is above this package's Node
floor.

Formatting goes through a hook bound to the active language because
reading the language straight off the i18next singleton put it outside
anything React tracks: a component with no reason of its own to
subscribe to a language change kept rendering the previous locale
indefinitely.

* UI: Address duration formatting review feedback

Relative times picked their unit from the unrounded gap and then rounded
inside it, so anything just short of the next unit printed that unit's
own ceiling: "60 minutes ago" where "an hour ago" was meant, and the
same at 24 hours and 12 months. Promoting after the rounding matches
what the duration path already does a few hundred lines up.

Two layout budgets were sized when every duration was the English
"1h 2m" and are too tight now that CLDR decides the width: the bar-end
labels on the slowest-task chart reserved a fixed 64px, and the Gantt
axis derived its tick spacing from an eight-character "HH:MM:SS"
estimate. German narrow needs roughly half again as much room.

The Gantt axis also placed ticks at evenly divided raw values, which
the old truncating format hid; at this precision a seven-second span
read 0s | 1.17s | 2.33s. Snapping to the same counted units the other
duration axes use keeps short spans legible.

Rounding to two units leaves "1h 2m" covering a full minute, which is
too coarse to tell two similar runs apart in a list. The exact value now
rides along in a title on the duration columns, so the compact form
stays scannable without giving up the precise number.

* Address PR Feedback

* UI: Export useDurationFormat from the utils barrel

The hook was the only util under src/utils that callers had to reach by
its own path, so consumers imported it differently from every other util
sitting beside it. Routing it through the barrel leaves one way in.

The Dag overview test replaces the barrel wholesale rather than patching
it, so it has to name the hook for the components under test to keep
resolving.
xvega pushed a commit to xvega/airflow that referenced this pull request Sep 13, 2026
* UI: Show durations in a readable, localized format

Zero-padded clock strings buried the one number that mattered: a task
that took 83ms read as 00:00:00.083, and anything under a second
rendered as 00:00:00 in chart tooltips and Gantt axis labels, which is
indistinguishable from no time at all. Someone scanning a column of
durations wants the magnitude at a glance, and roughly three
significant digits gives it to them at every scale from milliseconds
to days.

Unit names, decimal separators and plural forms now come from CLDR
through Intl rather than being hardcoded English, so durations follow
the language the rest of the UI is already using. This needs no new
translation keys, and it reproduces Intl.DurationFormat's narrow style
without requiring it, since that API is above this package's Node
floor.

Formatting goes through a hook bound to the active language because
reading the language straight off the i18next singleton put it outside
anything React tracks: a component with no reason of its own to
subscribe to a language change kept rendering the previous locale
indefinitely.

* UI: Address duration formatting review feedback

Relative times picked their unit from the unrounded gap and then rounded
inside it, so anything just short of the next unit printed that unit's
own ceiling: "60 minutes ago" where "an hour ago" was meant, and the
same at 24 hours and 12 months. Promoting after the rounding matches
what the duration path already does a few hundred lines up.

Two layout budgets were sized when every duration was the English
"1h 2m" and are too tight now that CLDR decides the width: the bar-end
labels on the slowest-task chart reserved a fixed 64px, and the Gantt
axis derived its tick spacing from an eight-character "HH:MM:SS"
estimate. German narrow needs roughly half again as much room.

The Gantt axis also placed ticks at evenly divided raw values, which
the old truncating format hid; at this precision a seven-second span
read 0s | 1.17s | 2.33s. Snapping to the same counted units the other
duration axes use keeps short spans legible.

Rounding to two units leaves "1h 2m" covering a full minute, which is
too coarse to tell two similar runs apart in a list. The exact value now
rides along in a title on the duration columns, so the compact form
stays scannable without giving up the precise number.

* Address PR Feedback

* UI: Export useDurationFormat from the utils barrel

The hook was the only util under src/utils that callers had to reach by
its own path, so consumers imported it differently from every other util
sitting beside it. Routing it through the barrel leaves one way in.

The Dag overview test replaces the barrel wholesale rather than patching
it, so it has to name the hook for the components under test to keep
resolving.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:UI Related to UI/UX. For Frontend Developers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants