Skip to content

Commit fc6b896

Browse files
committed
ls: Optimize time formatting
Instead of recreating the formatter over and over again, keep it pre-parsed in a variable in TimeStyler class. Also, avoid calling `now` over and over again, that's also slow. Improves performance by about 6%.
1 parent b833deb commit fc6b896

File tree

1 file changed

+62
-26
lines changed

1 file changed

+62
-26
lines changed

src/uu/ls/src/ls.rs

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use std::{
2727
use std::{collections::HashSet, io::IsTerminal};
2828

2929
use ansi_width::ansi_width;
30+
use chrono::format::{Item, StrftimeItems};
3031
use chrono::{DateTime, Local, TimeDelta};
3132
use clap::{
3233
Arg, ArgAction, Command,
@@ -273,32 +274,64 @@ enum TimeStyle {
273274
Format(String),
274275
}
275276

276-
/// Whether the given date is considered recent (i.e., in the last 6 months).
277-
fn is_recent(time: DateTime<Local>) -> bool {
278-
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
279-
time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now()
280-
}
277+
/// A struct/impl used to format a file DateTime, precomputing the format for performance reasons.
278+
struct TimeStyler {
279+
// default format, always specified.
280+
default: Vec<Item<'static>>,
281+
// format for a recent time, only specified it is is different from the default
282+
recent: Option<Vec<Item<'static>>>,
283+
// If `recent` is set, cache the threshold time when we switch from recent to default format.
284+
recent_time_threshold: Option<DateTime<Local>>,
285+
}
286+
287+
impl TimeStyler {
288+
/// Create a TimeStyler based on a TimeStyle specification.
289+
fn new(style: &TimeStyle) -> TimeStyler {
290+
let default: Vec<Item<'static>> = match style {
291+
TimeStyle::FullIso => StrftimeItems::new("%Y-%m-%d %H:%M:%S.%f %z").parse(),
292+
TimeStyle::LongIso => StrftimeItems::new("%Y-%m-%d %H:%M").parse(),
293+
TimeStyle::Iso => StrftimeItems::new("%Y-%m-%d ").parse(),
294+
// In this version of chrono translating can be done
295+
// The function is chrono::datetime::DateTime::format_localized
296+
// However it's currently still hard to get the current pure-rust-locale
297+
// So it's not yet implemented
298+
TimeStyle::Locale => StrftimeItems::new("%b %e %Y").parse(),
299+
TimeStyle::Format(fmt) => {
300+
// TODO (#7802): Replace with new_lenient
301+
StrftimeItems::new(custom_tz_fmt::custom_time_format(fmt).as_str()).parse_to_owned()
302+
}
303+
}
304+
.unwrap();
305+
let recent = match style {
306+
TimeStyle::Iso => Some(StrftimeItems::new("%m-%d %H:%M")),
307+
// See comment above about locale
308+
TimeStyle::Locale => Some(StrftimeItems::new("%b %e %H:%M")),
309+
_ => None,
310+
}
311+
.map(|x| x.collect());
312+
let recent_time_threshold = if recent.is_some() {
313+
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
314+
Some(Local::now() - TimeDelta::try_seconds(31_556_952 / 2).unwrap())
315+
} else {
316+
None
317+
};
281318

282-
impl TimeStyle {
283-
/// Format the given time according to this time format style.
319+
TimeStyler {
320+
default,
321+
recent,
322+
recent_time_threshold,
323+
}
324+
}
325+
326+
/// Format a DateTime, using `recent` format if available, and the DateTime
327+
/// is recent enough.
284328
fn format(&self, time: DateTime<Local>) -> String {
285-
let recent = is_recent(time);
286-
match (self, recent) {
287-
(Self::FullIso, _) => time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(),
288-
(Self::LongIso, _) => time.format("%Y-%m-%d %H:%M").to_string(),
289-
(Self::Iso, true) => time.format("%m-%d %H:%M").to_string(),
290-
(Self::Iso, false) => time.format("%Y-%m-%d ").to_string(),
291-
// spell-checker:ignore (word) datetime
292-
//In this version of chrono translating can be done
293-
//The function is chrono::datetime::DateTime::format_localized
294-
//However it's currently still hard to get the current pure-rust-locale
295-
//So it's not yet implemented
296-
(Self::Locale, true) => time.format("%b %e %H:%M").to_string(),
297-
(Self::Locale, false) => time.format("%b %e %Y").to_string(),
298-
(Self::Format(fmt), _) => time
299-
.format(custom_tz_fmt::custom_time_format(fmt).as_str())
300-
.to_string(),
329+
if self.recent.is_none() || time <= self.recent_time_threshold.unwrap() {
330+
time.format_with_items(self.default.iter())
331+
} else {
332+
time.format_with_items(self.recent.as_ref().unwrap().iter())
301333
}
334+
.to_string()
302335
}
303336
}
304337

@@ -2060,6 +2093,8 @@ struct ListState<'a> {
20602093
uid_cache: HashMap<u32, String>,
20612094
#[cfg(unix)]
20622095
gid_cache: HashMap<u32, String>,
2096+
2097+
time_styler: TimeStyler,
20632098
}
20642099

20652100
#[allow(clippy::cognitive_complexity)]
@@ -2076,6 +2111,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
20762111
uid_cache: HashMap::new(),
20772112
#[cfg(unix)]
20782113
gid_cache: HashMap::new(),
2114+
time_styler: TimeStyler::new(&config.time_style),
20792115
};
20802116

20812117
for loc in locs {
@@ -2876,7 +2912,7 @@ fn display_item_long(
28762912
};
28772913

28782914
output_display.extend(b" ");
2879-
output_display.extend(display_date(md, config).as_bytes());
2915+
output_display.extend(display_date(md, config, state).as_bytes());
28802916
output_display.extend(b" ");
28812917

28822918
let item_name = display_item_name(
@@ -3080,9 +3116,9 @@ fn get_time(md: &Metadata, config: &Config) -> Option<DateTime<Local>> {
30803116
Some(time.into())
30813117
}
30823118

3083-
fn display_date(metadata: &Metadata, config: &Config) -> String {
3119+
fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String {
30843120
match get_time(metadata, config) {
3085-
Some(time) => config.time_style.format(time),
3121+
Some(time) => state.time_styler.format(time),
30863122
None => "???".into(),
30873123
}
30883124
}

0 commit comments

Comments
 (0)