-
-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tail latency issue on reads and writes due to slow run_pending_tasks
#498
Comments
Hi @texascloud Thank you for reporting the issue. I moved your comment to this new issue from #79 (comment).
Yes, please share the code. I cannot tell what is going on without knowing more details. For example, is your cache has the eviction listener?
Thanks! |
Never mind. I realized that it was taken from https://github.com/moka-rs/moka/blob/v0.12.10/src/cht/map/bucket.rs#L733C1-L733C54 |
You are talking about this code, right? if tbc >= 25_000.0 || tbc / real_cap >= 0.1
I am not sure because I do not know what your benchmark program is doing. I will check when you share the code of the benchmark. |
Thanks for turning that comment into an issue! I have created a gist with my minimal repro as a CLI such that you can tweak the different values and observe the longest time an eviction takes based on those values. https://gist.github.com/texascloud/8d16f92c9cbae3a220340a7abc472ea3 |
FWIW it doesn't matter whether The way the re-balancing is manifesting feels like a "pause-the-world" GC, which gives unpredictably large tail latency. The fact If eviction had big duration spikes but didn't affect read/write perf of the cache, I wouldn't really care. It's important to note too that I also tried setting both sync and async eviction listeners as a way to force the batch eviction logic + benefit from the 100ms duration timeout. That did not work since the re-balancing, once triggered, will eat up 1,500+ ms synchronously. There is no way for the timeout to cut short the re-balancing. The eviction listener I added was a no-op too. |
Hi. Thanks a lot for providing the minimal repro. I can reproduce the issue. I see some spikes in the elapsed time for the 2025-02-19T12:00:23.950905Z INFO moka_test: longest sync time seen: 976ms moka::future::Cache result$ cargo run --release -- --entries 5000000 --time-till-inserts-done-ms 16000 --poll-till-empty-ms 1000
2025-02-19T11:59:18.637474Z INFO moka_test: [serial] inserting 5000000 items
2025-02-19T11:59:22.949546Z INFO moka_test: done after 4312ms - size: 4999936
2025-02-19T11:59:22.949588Z INFO moka_test: [before sync] size: 5000000
2025-02-19T11:59:35.065602Z INFO moka_test: [polling till empty] num_during_iter: 5000000 - removed: cycle/168998 total/168998 - elapsed_ms: 114 - items/ms: 1482
2025-02-19T11:59:37.035270Z INFO moka_test: [polling till empty] num_during_iter: 4831002 - removed: cycle/169000 total/337998 - elapsed_ms: 84 - items/ms: 2011
2025-02-19T11:59:39.010473Z INFO moka_test: [polling till empty] num_during_iter: 4662002 - removed: cycle/169000 total/506998 - elapsed_ms: 58 - items/ms: 2913
2025-02-19T11:59:40.029596Z INFO moka_test: [polling till empty] num_during_iter: 4493002 - removed: cycle/169000 total/675998 - elapsed_ms: 78 - items/ms: 2166
2025-02-19T11:59:42.927379Z INFO moka_test: [polling till empty] num_during_iter: 4324002 - removed: cycle/169000 total/844998 - elapsed_ms: 976 - items/ms: 173
2025-02-19T11:59:43.315642Z INFO moka_test: [polling till empty] num_during_iter: 4155002 - removed: cycle/169000 total/1013998 - elapsed_ms: 364 - items/ms: 464
2025-02-19T11:59:45.152806Z INFO moka_test: [polling till empty] num_during_iter: 3986002 - removed: cycle/169000 total/1182998 - elapsed_ms: 201 - items/ms: 840
2025-02-19T11:59:45.995010Z INFO moka_test: [polling till empty] num_during_iter: 3817002 - removed: cycle/61311 total/1244309 - elapsed_ms: 44 - items/ms: 1393
2025-02-19T11:59:47.004119Z INFO moka_test: [polling till empty] num_during_iter: 3755691 - removed: cycle/107689 total/1351998 - elapsed_ms: 52 - items/ms: 2070
2025-02-19T11:59:48.034743Z INFO moka_test: [polling till empty] num_during_iter: 3648002 - removed: cycle/169000 total/1520998 - elapsed_ms: 84 - items/ms: 2011
2025-02-19T11:59:50.861371Z INFO moka_test: [polling till empty] num_during_iter: 3479002 - removed: cycle/169000 total/1689998 - elapsed_ms: 910 - items/ms: 185
2025-02-19T11:59:51.088165Z INFO moka_test: [polling till empty] num_during_iter: 3310002 - removed: cycle/41331 total/1731329 - elapsed_ms: 136 - items/ms: 303
2025-02-19T11:59:52.082248Z INFO moka_test: [polling till empty] num_during_iter: 3268671 - removed: cycle/127669 total/1858998 - elapsed_ms: 130 - items/ms: 982
2025-02-19T11:59:54.046885Z INFO moka_test: [polling till empty] num_during_iter: 3141002 - removed: cycle/169000 total/2027998 - elapsed_ms: 95 - items/ms: 1778
2025-02-19T11:59:55.022306Z INFO moka_test: [polling till empty] num_during_iter: 2972002 - removed: cycle/169000 total/2196998 - elapsed_ms: 71 - items/ms: 2380
2025-02-19T11:59:55.982467Z INFO moka_test: [polling till empty] num_during_iter: 2803002 - removed: cycle/27992 total/2224990 - elapsed_ms: 31 - items/ms: 902
2025-02-19T11:59:57.007788Z INFO moka_test: [polling till empty] num_during_iter: 2775010 - removed: cycle/141008 total/2365998 - elapsed_ms: 56 - items/ms: 2518
2025-02-19T11:59:58.594269Z INFO moka_test: [polling till empty] num_during_iter: 2634002 - removed: cycle/169000 total/2534998 - elapsed_ms: 643 - items/ms: 262
2025-02-19T12:00:00.201893Z INFO moka_test: [polling till empty] num_during_iter: 2465002 - removed: cycle/169000 total/2703998 - elapsed_ms: 250 - items/ms: 676
2025-02-19T12:00:01.055792Z INFO moka_test: [polling till empty] num_during_iter: 2296002 - removed: cycle/169000 total/2872998 - elapsed_ms: 104 - items/ms: 1625
2025-02-19T12:00:03.278619Z INFO moka_test: [polling till empty] num_during_iter: 2127002 - removed: cycle/68834 total/2941832 - elapsed_ms: 326 - items/ms: 211
2025-02-19T12:00:04.181376Z INFO moka_test: [polling till empty] num_during_iter: 2058168 - removed: cycle/100166 total/3041998 - elapsed_ms: 231 - items/ms: 433
2025-02-19T12:00:05.017919Z INFO moka_test: [polling till empty] num_during_iter: 1958002 - removed: cycle/169000 total/3210998 - elapsed_ms: 66 - items/ms: 2560
2025-02-19T12:00:08.414076Z INFO moka_test: [polling till empty] num_during_iter: 1789002 - removed: cycle/218346 total/3429344 - elapsed_ms: 463 - items/ms: 471
2025-02-19T12:00:09.016506Z INFO moka_test: [polling till empty] num_during_iter: 1570656 - removed: cycle/119654 total/3548998 - elapsed_ms: 65 - items/ms: 1840
2025-02-19T12:00:10.026342Z INFO moka_test: [polling till empty] num_during_iter: 1451002 - removed: cycle/169000 total/3717998 - elapsed_ms: 74 - items/ms: 2283
2025-02-19T12:00:12.390498Z INFO moka_test: [polling till empty] num_during_iter: 1282002 - removed: cycle/169000 total/3886998 - elapsed_ms: 440 - items/ms: 384
2025-02-19T12:00:13.250008Z INFO moka_test: [polling till empty] num_during_iter: 1113002 - removed: cycle/169000 total/4055998 - elapsed_ms: 298 - items/ms: 567
2025-02-19T12:00:15.188425Z INFO moka_test: [polling till empty] num_during_iter: 944002 - removed: cycle/169000 total/4224998 - elapsed_ms: 237 - items/ms: 713
2025-02-19T12:00:15.994421Z INFO moka_test: [polling till empty] num_during_iter: 775002 - removed: cycle/45464 total/4270462 - elapsed_ms: 43 - items/ms: 1057
2025-02-19T12:00:17.059822Z INFO moka_test: [polling till empty] num_during_iter: 729538 - removed: cycle/123536 total/4393998 - elapsed_ms: 108 - items/ms: 1143
2025-02-19T12:00:18.229627Z INFO moka_test: [polling till empty] num_during_iter: 606002 - removed: cycle/169000 total/4562998 - elapsed_ms: 278 - items/ms: 607
2025-02-19T12:00:20.150004Z INFO moka_test: [polling till empty] num_during_iter: 437002 - removed: cycle/169000 total/4731998 - elapsed_ms: 199 - items/ms: 849
2025-02-19T12:00:21.132597Z INFO moka_test: [polling till empty] num_during_iter: 268002 - removed: cycle/169000 total/4900998 - elapsed_ms: 181 - items/ms: 933
2025-02-19T12:00:23.030711Z INFO moka_test: [polling till empty] num_during_iter: 99002 - removed: cycle/99002 total/5000000 - elapsed_ms: 80 - items/ms: 1237
2025-02-19T12:00:23.950851Z INFO moka_test: total time syncing cache: 7570ms
2025-02-19T12:00:23.950905Z INFO moka_test: longest sync time seen: 976ms
2025-02-19T12:00:23.950912Z INFO moka_test: [after sync] size: 0 - elapsed_ms: 61001
2025-02-19T12:00:23.950918Z INFO moka_test: total duration: 65314ms
That is right. I am currently replacing https://github.com/ibraheemdev/papaya?tab=readme-ov-file#performance I have only done some work with 2025-02-19T13:24:18.530186Z INFO moka_test: longest sync time seen: 227ms As you can see, the elapse times are more stable. Also, moka::sync::Cache with papaya025-02-19T13:23:10.391588Z INFO moka_test: [serial] inserting 5000000 items
2025-02-19T13:23:17.528165Z INFO moka_test: done after 7136ms - size: 4999936
2025-02-19T13:23:17.528186Z INFO moka_test: [before sync] size: 5000000
2025-02-19T13:23:26.583867Z INFO moka_test: [polling till empty] num_during_iter: 5000000 - removed: cycle/168998 total/168998 - elapsed_ms: 53 - items/ms: 3188
2025-02-19T13:23:28.647459Z INFO moka_test: [polling till empty] num_during_iter: 4831002 - removed: cycle/169000 total/337998 - elapsed_ms: 117 - items/ms: 1444
2025-02-19T13:23:29.562388Z INFO moka_test: [polling till empty] num_during_iter: 4662002 - removed: cycle/15680 total/353678 - elapsed_ms: 29 - items/ms: 540
2025-02-19T13:23:30.614620Z INFO moka_test: [polling till empty] num_during_iter: 4646322 - removed: cycle/153320 total/506998 - elapsed_ms: 84 - items/ms: 1825
2025-02-19T13:23:31.617589Z INFO moka_test: [polling till empty] num_during_iter: 4493002 - removed: cycle/169000 total/675998 - elapsed_ms: 87 - items/ms: 1942
2025-02-19T13:23:33.637911Z INFO moka_test: [polling till empty] num_during_iter: 4324002 - removed: cycle/169000 total/844998 - elapsed_ms: 108 - items/ms: 1564
2025-02-19T13:23:34.595553Z INFO moka_test: [polling till empty] num_during_iter: 4155002 - removed: cycle/65085 total/910083 - elapsed_ms: 65 - items/ms: 1001
2025-02-19T13:23:35.608686Z INFO moka_test: [polling till empty] num_during_iter: 4089917 - removed: cycle/103915 total/1013998 - elapsed_ms: 78 - items/ms: 1332
2025-02-19T13:23:36.653665Z INFO moka_test: [polling till empty] num_during_iter: 3986002 - removed: cycle/169000 total/1182998 - elapsed_ms: 123 - items/ms: 1373
2025-02-19T13:23:38.650928Z INFO moka_test: [polling till empty] num_during_iter: 3817002 - removed: cycle/169000 total/1351998 - elapsed_ms: 120 - items/ms: 1408
2025-02-19T13:23:39.555550Z INFO moka_test: [polling till empty] num_during_iter: 3648002 - removed: cycle/1042 total/1353040 - elapsed_ms: 25 - items/ms: 41
2025-02-19T13:23:40.660179Z INFO moka_test: [polling till empty] num_during_iter: 3646960 - removed: cycle/167958 total/1520998 - elapsed_ms: 130 - items/ms: 1291
2025-02-19T13:23:41.646587Z INFO moka_test: [polling till empty] num_during_iter: 3479002 - removed: cycle/169000 total/1689998 - elapsed_ms: 116 - items/ms: 1456
2025-02-19T13:23:43.671963Z INFO moka_test: [polling till empty] num_during_iter: 3310002 - removed: cycle/169000 total/1858998 - elapsed_ms: 141 - items/ms: 1198
2025-02-19T13:23:45.682960Z INFO moka_test: [polling till empty] num_during_iter: 3141002 - removed: cycle/169000 total/2027998 - elapsed_ms: 154 - items/ms: 1097
2025-02-19T13:23:46.622202Z INFO moka_test: [polling till empty] num_during_iter: 2972002 - removed: cycle/69134 total/2097132 - elapsed_ms: 92 - items/ms: 751
2025-02-19T13:23:47.634510Z INFO moka_test: [polling till empty] num_during_iter: 2902868 - removed: cycle/99866 total/2196998 - elapsed_ms: 104 - items/ms: 960
2025-02-19T13:23:48.682305Z INFO moka_test: [polling till empty] num_during_iter: 2803002 - removed: cycle/169000 total/2365998 - elapsed_ms: 152 - items/ms: 1111
2025-02-19T13:23:50.674933Z INFO moka_test: [polling till empty] num_during_iter: 2634002 - removed: cycle/169000 total/2534998 - elapsed_ms: 144 - items/ms: 1173
2025-02-19T13:23:52.672605Z INFO moka_test: [polling till empty] num_during_iter: 2465002 - removed: cycle/169000 total/2703998 - elapsed_ms: 142 - items/ms: 1190
2025-02-19T13:23:53.670211Z INFO moka_test: [polling till empty] num_during_iter: 2296002 - removed: cycle/88497 total/2792495 - elapsed_ms: 141 - items/ms: 627
2025-02-19T13:23:54.622296Z INFO moka_test: [polling till empty] num_during_iter: 2207505 - removed: cycle/80503 total/2872998 - elapsed_ms: 92 - items/ms: 875
2025-02-19T13:23:55.674065Z INFO moka_test: [polling till empty] num_during_iter: 2127002 - removed: cycle/169000 total/3041998 - elapsed_ms: 144 - items/ms: 1173
2025-02-19T13:23:57.682480Z INFO moka_test: [polling till empty] num_during_iter: 1958002 - removed: cycle/169000 total/3210998 - elapsed_ms: 152 - items/ms: 1111
2025-02-19T13:23:59.685746Z INFO moka_test: [polling till empty] num_during_iter: 1789002 - removed: cycle/169000 total/3379998 - elapsed_ms: 156 - items/ms: 1083
2025-02-19T13:24:00.611376Z INFO moka_test: [polling till empty] num_during_iter: 1620002 - removed: cycle/76780 total/3456778 - elapsed_ms: 81 - items/ms: 947
2025-02-19T13:24:01.619402Z INFO moka_test: [polling till empty] num_during_iter: 1543222 - removed: cycle/92220 total/3548998 - elapsed_ms: 90 - items/ms: 1024
2025-02-19T13:24:02.696418Z INFO moka_test: [polling till empty] num_during_iter: 1451002 - removed: cycle/169000 total/3717998 - elapsed_ms: 167 - items/ms: 1011
2025-02-19T13:24:04.701066Z INFO moka_test: [polling till empty] num_during_iter: 1282002 - removed: cycle/169000 total/3886998 - elapsed_ms: 170 - items/ms: 994
2025-02-19T13:24:06.714963Z INFO moka_test: [polling till empty] num_during_iter: 1113002 - removed: cycle/169000 total/4055998 - elapsed_ms: 185 - items/ms: 913
2025-02-19T13:24:08.706211Z INFO moka_test: [polling till empty] num_during_iter: 944002 - removed: cycle/169000 total/4224998 - elapsed_ms: 177 - items/ms: 954
2025-02-19T13:24:09.562664Z INFO moka_test: [polling till empty] num_during_iter: 775002 - removed: cycle/2523 total/4227521 - elapsed_ms: 33 - items/ms: 76
2025-02-19T13:24:10.757244Z INFO moka_test: [polling till empty] num_during_iter: 772479 - removed: cycle/166477 total/4393998 - elapsed_ms: 227 - items/ms: 733
2025-02-19T13:24:12.715065Z INFO moka_test: [polling till empty] num_during_iter: 606002 - removed: cycle/169000 total/4562998 - elapsed_ms: 185 - items/ms: 913
2025-02-19T13:24:13.709420Z INFO moka_test: [polling till empty] num_during_iter: 437002 - removed: cycle/169000 total/4731998 - elapsed_ms: 179 - items/ms: 944
2025-02-19T13:24:15.720519Z INFO moka_test: [polling till empty] num_during_iter: 268002 - removed: cycle/169000 total/4900998 - elapsed_ms: 191 - items/ms: 884
2025-02-19T13:24:17.674445Z INFO moka_test: [polling till empty] num_during_iter: 99002 - removed: cycle/99002 total/5000000 - elapsed_ms: 145 - items/ms: 682
2025-02-19T13:24:18.530160Z INFO moka_test: total time syncing cache: 4579ms
2025-02-19T13:24:18.530186Z INFO moka_test: longest sync time seen: 227ms
2025-02-19T13:24:18.530205Z INFO moka_test: [after sync] size: 0 - elapsed_ms: 61002
2025-02-19T13:24:18.530209Z INFO moka_test: total duration: 68140ms You can try it by yourself by the following steps:
|
Thanks for taking a look! I realized I needed to perform reads/writes on the cache while the expirations were happening. I only focused on reads for one benchmark as I primarily read from the cache. This is the code I added: Spawned thread to read every 1ms and print measurements
let cache = moka::future::Cache::<String, u64>::builder()
.weigher(|_key: &String, value: &u64| -> u32 { size_of_val(value) as u32 })
.initial_capacity(131_072)
.max_capacity(10_000_000_000)
.eviction_policy(moka::policy::EvictionPolicy::lru())
.expire_after(ExpirationPolicy::new(
Duration::from_secs(16),
cmd.eviction_batch_size - 1,
))
.build();
let read_cache = cache.clone();
let (read_tx, mut rx) = tokio::sync::oneshot::channel::<bool>();
// A task which constantly reads/writes to the cache and measures
// the max time.
let max_read_latency = tokio::spawn(async move {
let mut max_latency = 0;
let mut reads = 0;
let mut slow_reads = 0;
let mut total_elapsed_ms = 0;
let mut derp = 0;
loop {
if rx.try_recv().is_ok() {
break;
}
let start = std::time::Instant::now();
let _ = read_cache.get("100").await;
let elapsed = start.elapsed();
let elapsed_ms = elapsed.as_millis();
reads += 1;
total_elapsed_ms += elapsed_ms;
derp += elapsed.as_micros();
if total_elapsed_ms > 1000 {
eprintln!("reads: {reads}");
total_elapsed_ms -= 1000;
}
max_latency = max_latency.max(elapsed_ms as u64);
if elapsed_ms > 300 {
slow_reads += 1;
eprintln!("read took {:?}ms", elapsed_ms);
}
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
if reads > 0 && slow_reads > 0 {
eprintln!(
"slow reads: {slow_reads} - total reads: {reads} - accum read time: {derp}us - avg read time: {:.4}us - slow reads %: {:.2}",
derp / reads,
(slow_reads as f64 / reads as f64) * 100.0
);
}
max_latency
});
// ... other code that polls till cache is empty .....
read_tx.send(true).unwrap();
let max_read_latency = max_read_latency.await?;
tracing::info!("max_read_latency: {:?}", max_read_latency); The code above shows the real impact of the GC-style rehashing of the
I did an experiment where I used For comparison, the sync test I did with
I am excited to see you are exploring an alternative to the existing For context, here's what kind of workload I am using Moka for: I am interested in trying Moka with |
I have not had a chance to try the new code, but I will do so this weekend. (It is Friday night here in the China mainland.) If |
This is interesting, 227ms is still longer than I expect for papaya with the default seize batch size. I wonder if it's related to an issue I've noticed where the |
My current random thoughts. What is happening?
The slow reads What can be done to fix or mitigate the issues?
|
I copied the repro to here and will start to modify: |
I confirmed this is true. With implicit
|
future::Cache::run_pending_task
method occasionally takes significantly longer under heavy writes when Expiry
is set (?)run_pending_tasks
Yesterday, I pushed a commit to the above repository to replace $ cargo run --bin main3-papaya --release -- --entries 5000000 --poll-till-empty-ms 1000 \
--no-run-pending-tasks However, I got similar result to the one with Anyway, I started to work on the following today.
But instead of providing such an option to It might look like the following: let cfg = MaintenanceConfig::builder()
// Configures the `run_pending_tasks` implicitly called within a read
// operation such as `get` and `get_with`.
.auto_run_on_reads(
AutoRunConfig::builder()
// Default is to repeat the following 4 times:
//
// 1. Process all pending read logs.
// 2. Do other stuff (e.g. evictions).
// 3. Go to 1.
.process_pending_read_logs(ProcessingConfig::MaxCount(
NonZeroUsize::new(1_000).unwrap(),
))
// Default is the same as `process_pending_read_logs`.
.process_pending_write_logs(ProcessingConfig::Disabled)
// Default is `Unlimited`.
.process_expired_entries(ProcessingConfig::Disabled)
// Default is `Unlimited`.
.process_invalidated_entries(ProcessingConfig::Disabled)
.build(),
)
// Configures the `run_pending_tasks` implicitly called within a write
// operation such as `insert`, `remove` or `get_with` resulting an insertion.
.auto_run_on_writes(
AutoRunConfig::builder()
.process_pending_read_logs(ProcessingConfig::MaxCount(
NonZeroUsize::new(10_000).unwrap(),
))
.process_pending_write_logs(ProcessingConfig::MaxCount(
NonZeroUsize::new(1_000).unwrap(),
))
.process_expired_entries(ProcessingConfig::MaxCount(
NonZeroUsize::new(2_000).unwrap(),
))
.process_invalidated_entries(ProcessingConfig::Disabled)
.build(),
)
// Configures the operation logs that need to be processed.
.operation_logs(
OperationLogsConfig::builder()
// The maximum number of pending read logs that can be stored.
// If the number of pending read logs exceeds this value, new
// read logs will be discarded. Default is `384`
.max_pending_read_logs(NonZeroUsize::new(10_000).unwrap())
// The maximum number of pending write logs that can be stored.
// If the number of pending write logs exceeds this value, write
// operations will be blocked. Default is `384`.
.max_pending_write_logs(NonZeroUsize::new(1_000).unwrap())
// Configures the triggers that will cause the `run_pending_tasks`
// method to be implicitly called within a read or write operation.
.trigger_auto_run_when(
AutoRunTriggers::builder()
// This trigger fires when the number of pending read logs
// exceeds the specified value. Default is `64`.
.pending_read_logs_exceed(NonZeroUsize::new(1_000).unwrap())
// This trigger fires when the number of pending write logs
// exceeds the specified value. Default is `64`.
.pending_write_logs_exceed(NonZeroUsize::new(250).unwrap())
// This trigger fires when the time past since the last
// `run_pending_tasks` call exceeds the specified duration.
// Set to `None` to disable this trigger. Default is `300ms`.
.time_past(Some(Duration::from_millis(300)))
.build(),
)
.build(),
)
.build();
let cache = Cache::<String, u64>::builder()
.weigher(|_key: &String, value: &u64| -> u32 { size_of_val(value) as u32 })
.initial_capacity(131_072)
.max_capacity(10_000_000_000)
.eviction_policy(EvictionPolicy::lru())
.expire_after(...))
.maintenance_config(cfg) // Added.
.build(); Doing this should be much easier than providing an option to |
@tatsuya6502 Amazing investigation! I really like the histogram view of the read latencies. It provides a clear picture of the improvements.
This sounds like a good solution to me. Curious to see if it fully addresses the perf issues I was having since I will still be calling
FWIW, if I am reading the code correctly, creating an async eviction listener which simply runs I would fully welcome being able to set a max number of entries to evict per Does limiting the number of entries to evict per run make the re-hash less impactful when it does run? |
Is this example configuration one I would use to disable most of the expensive things in |
Originally posted by @texascloud in #79 (comment)
The text was updated successfully, but these errors were encountered: