diff --git a/benchmarks/pandas/bench_ewm.py b/benchmarks/pandas/bench_ewm.py new file mode 100644 index 00000000..cf2fd4fa --- /dev/null +++ b/benchmarks/pandas/bench_ewm.py @@ -0,0 +1,23 @@ +"""Benchmark: ewm (Exponentially Weighted Moving) aggregations on 100k-element pandas Series""" +import json, time, math +import numpy as np +import pandas as pd + +ROWS = 100_000 +WARMUP = 3 +ITERATIONS = 10 +data = [math.sin(i * 0.01) * 100 + 50 for i in range(ROWS)] +s = pd.Series(data) + +for _ in range(WARMUP): + s.ewm(span=20).mean() + s.ewm(span=20).std() + s.ewm(span=20).var() + +start = time.perf_counter() +for _ in range(ITERATIONS): + s.ewm(span=20).mean() + s.ewm(span=20).std() + s.ewm(span=20).var() +total = (time.perf_counter() - start) * 1000 +print(json.dumps({"function": "ewm", "mean_ms": total / ITERATIONS, "iterations": ITERATIONS, "total_ms": total})) diff --git a/benchmarks/pandas/bench_multi_index_to_list.py b/benchmarks/pandas/bench_multi_index_to_list.py new file mode 100644 index 00000000..4d206234 --- /dev/null +++ b/benchmarks/pandas/bench_multi_index_to_list.py @@ -0,0 +1,20 @@ +"""Benchmark: MultiIndex.tolist() on 100k-pair MultiIndex""" +import json, time +import pandas as pd + +ROWS = 100_000 +WARMUP = 3 +ITERATIONS = 10 +a = [f"a{i % 100}" for i in range(ROWS)] +b = [i % 1000 for i in range(ROWS)] +tuples = list(zip(a, b)) +mi = pd.MultiIndex.from_tuples(tuples) + +for _ in range(WARMUP): + mi.tolist() + +start = time.perf_counter() +for _ in range(ITERATIONS): + mi.tolist() +total = (time.perf_counter() - start) * 1000 +print(json.dumps({"function": "multi_index_to_list", "mean_ms": total / ITERATIONS, "iterations": ITERATIONS, "total_ms": total})) diff --git a/benchmarks/pandas/bench_register_option.py b/benchmarks/pandas/bench_register_option.py new file mode 100644 index 00000000..94cc07e3 --- /dev/null +++ b/benchmarks/pandas/bench_register_option.py @@ -0,0 +1,79 @@ +""" +Benchmark: register_option — register custom options with pandas' options system. + +Mirrors tsb registerOption which wraps pandas' core config register_option API. +Uses pandas.core.config_init / _config._registered_options to register custom +options with defaults and validators. + +Outputs JSON: {"function": "register_option", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time + +import pandas as pd + +WARMUP = 5 +ITERATIONS = 1_000 + +key_counter = [0] + + +def register_and_exercise(): + key = f"bench.custom_{key_counter[0]}" + key_counter[0] += 1 + # pandas does not expose a public register_option in the top-level namespace, + # but it is accessible via pd.core.config.register_option (internal API). + # We simulate the equivalent pattern: register → get → set → reset. + try: + pd.core.config.register_option(key, 42, "A custom numeric option for benchmarking.") + except Exception: + pass # already registered or unavailable + try: + v = pd.get_option(key) + pd.set_option(key, 99) + pd.reset_option(key) + _ = v + except Exception: + pass + + +def register_with_validator(): + key = f"bench.validated_{key_counter[0]}" + key_counter[0] += 1 + + def validator(val): + if not isinstance(val, (int, float)) or val < 0: + raise ValueError("must be a non-negative number") + + try: + pd.core.config.register_option(key, 10, "A validated option.", validator=validator) + except Exception: + pass + try: + pd.set_option(key, 50) + pd.reset_option(key) + except Exception: + pass + + +# Warm-up +for _ in range(WARMUP): + register_and_exercise() + register_with_validator() + +start = time.perf_counter() +for _ in range(ITERATIONS): + register_and_exercise() + register_with_validator() +total_ms = (time.perf_counter() - start) * 1000 + +print( + json.dumps( + { + "function": "register_option", + "mean_ms": total_ms / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total_ms, + } + ) +) diff --git a/benchmarks/pandas/bench_string_array_str_ops.py b/benchmarks/pandas/bench_string_array_str_ops.py new file mode 100644 index 00000000..565318d1 --- /dev/null +++ b/benchmarks/pandas/bench_string_array_str_ops.py @@ -0,0 +1,47 @@ +""" +Benchmark: StringArray additional string operations — +lstrip, rstrip, startswith, endswith, replace, zfill +on a 100k-element nullable StringDtype array (~10 % nulls). + +Mirrors pandas pd.array([...], dtype="string") str methods: + str.lstrip, str.rstrip, str.startswith, str.endswith, str.replace, str.zfill + +Outputs JSON: {"function": "string_array_str_ops", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import pandas as pd + +N = 100_000 +WARMUP = 3 +ITERATIONS = 50 + +WORDS = [" hello world ", " foo bar ", "baz qux ", " quux", "corge", "grault ", "garply"] +raw = [None if i % 10 == 0 else WORDS[i % len(WORDS)] for i in range(N)] + +a = pd.array(raw, dtype="string") + + +def run() -> None: + a.str.lstrip() + a.str.rstrip() + a.str.startswith(" he") + a.str.endswith("ld ") + a.str.replace("hello", "hi", regex=False) + a.str.zfill(12) + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "string_array_str_ops", + "mean_ms": total_ms / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_to_dict_series_orient.py b/benchmarks/pandas/bench_to_dict_series_orient.py new file mode 100644 index 00000000..cbb069ce --- /dev/null +++ b/benchmarks/pandas/bench_to_dict_series_orient.py @@ -0,0 +1,38 @@ +""" +Benchmark: DataFrame.to_dict(orient="series") — converts each column to a pandas Series. + +Mirrors tsb toDictOriented(df, "series"). + +Outputs JSON: {"function": "to_dict_series_orient", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import numpy as np +import pandas as pd + +ROWS = 10_000 +WARMUP = 5 +ITERATIONS = 30 + +df = pd.DataFrame({ + "id": np.arange(ROWS), + "value": np.arange(ROWS) * 1.5, + "label": [f"item_{i % 100}" for i in range(ROWS)], + "score": np.sin(np.arange(ROWS) * 0.01) * 100, + "flag": np.arange(ROWS) % 2 == 0, +}) + +for _ in range(WARMUP): + df.to_dict(orient="series") + +t0 = time.perf_counter() +for _ in range(ITERATIONS): + df.to_dict(orient="series") +total = (time.perf_counter() - t0) * 1000 + +print(json.dumps({ + "function": "to_dict_series_orient", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_wasm_agg_ops.py b/benchmarks/pandas/bench_wasm_agg_ops.py new file mode 100644 index 00000000..0b44ec3b --- /dev/null +++ b/benchmarks/pandas/bench_wasm_agg_ops.py @@ -0,0 +1,51 @@ +""" +Benchmark: numpy aggregate operations — np.sum, np.mean, np.min, np.max, np.var, np.std, np.median +plus pandas rolling and expanding window ops on a 100k-element float64 array. + +Mirrors tsb bench_wasm_agg_ops.ts. + +Outputs JSON: {"function": "wasm_agg_ops", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import numpy as np +import pandas as pd + +SIZE = 100_000 +WINDOW = 50 +MIN_PERIODS = 1 +WARMUP = 3 +ITERATIONS = 20 + +data = np.sin(np.arange(SIZE) * 0.001) * 1000 +series = pd.Series(data) + + +def run(): + np.sum(data) + np.mean(data) + np.min(data) + np.max(data) + np.var(data, ddof=1) + np.std(data, ddof=1) + np.median(data) + series.rolling(window=WINDOW, min_periods=MIN_PERIODS).sum() + series.rolling(window=WINDOW, min_periods=MIN_PERIODS).mean() + series.expanding(min_periods=MIN_PERIODS).sum() + series.expanding(min_periods=MIN_PERIODS).mean() + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total = (time.perf_counter() - start) * 1000 # ms + +print(json.dumps({ + "function": "wasm_agg_ops", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_wasm_rolling_stats.py b/benchmarks/pandas/bench_wasm_rolling_stats.py new file mode 100644 index 00000000..a495aed1 --- /dev/null +++ b/benchmarks/pandas/bench_wasm_rolling_stats.py @@ -0,0 +1,64 @@ +""" +Benchmark: WASM rolling/expanding stats equivalents using pandas/numpy — +Series.rolling(50).min/max/var/std/median and Series.expanding().min/max/var/std/median +on a 100k-element float64 array. + +Mirrors bench_wasm_rolling_stats.ts + +Outputs JSON: {"function": "wasm_rolling_stats", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" + +import json +import math +import time + +import numpy as np +import pandas as pd + +SIZE = 100_000 +WINDOW = 50 +MIN_PERIODS = 1 +WARMUP = 3 +ITERATIONS = 20 + +# Deterministic float64 data (same as TS counterpart) +data = np.array( + [math.sin(i * 0.001) * 100 + math.cos(i * 0.003) * 50 for i in range(SIZE)], + dtype=np.float64, +) +s = pd.Series(data) + + +def run_once() -> None: + s.rolling(WINDOW, min_periods=MIN_PERIODS).min() + s.rolling(WINDOW, min_periods=MIN_PERIODS).max() + s.rolling(WINDOW, min_periods=MIN_PERIODS).var() + s.rolling(WINDOW, min_periods=MIN_PERIODS).std() + s.rolling(WINDOW, min_periods=MIN_PERIODS).median() + s.expanding(min_periods=MIN_PERIODS).min() + s.expanding(min_periods=MIN_PERIODS).max() + s.expanding(min_periods=MIN_PERIODS).var() + s.expanding(min_periods=MIN_PERIODS).std() + s.expanding(min_periods=MIN_PERIODS).median() + + +# Warm-up +for _ in range(WARMUP): + run_once() + +# Measured iterations +t0 = time.perf_counter() +for _ in range(ITERATIONS): + run_once() +total_ms = (time.perf_counter() - t0) * 1000 + +print( + json.dumps( + { + "function": "wasm_rolling_stats", + "mean_ms": total_ms / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total_ms, + } + ) +) diff --git a/benchmarks/tsb/bench_ewm.ts b/benchmarks/tsb/bench_ewm.ts new file mode 100644 index 00000000..de980ee9 --- /dev/null +++ b/benchmarks/tsb/bench_ewm.ts @@ -0,0 +1,34 @@ +/** + * Benchmark: EWM (Exponentially Weighted Moving) aggregations on 100k-element Series + */ +import { Series } from "../../src/index.js"; + +const ROWS = 100_000; +const WARMUP = 3; +const ITERATIONS = 10; +const data = Array.from({ length: ROWS }, (_, i) => Math.sin(i * 0.01) * 100 + 50); +const s = new Series({ data }); + +// Warm-up: ewm mean, std, var with span=20 +for (let i = 0; i < WARMUP; i++) { + s.ewm({ span: 20 }).mean(); + s.ewm({ span: 20 }).std(); + s.ewm({ span: 20 }).var(); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + s.ewm({ span: 20 }).mean(); + s.ewm({ span: 20 }).std(); + s.ewm({ span: 20 }).var(); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "ewm", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_multi_index_to_list.ts b/benchmarks/tsb/bench_multi_index_to_list.ts new file mode 100644 index 00000000..e367fdf5 --- /dev/null +++ b/benchmarks/tsb/bench_multi_index_to_list.ts @@ -0,0 +1,26 @@ +/** + * Benchmark: MultiIndex.toList() on 100k-pair MultiIndex + * Outputs JSON: {"function": "multi_index_to_list", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { MultiIndex } from "../../src/index.js"; + +const ROWS = 100_000; +const WARMUP = 3; +const ITERATIONS = 10; +const a = Array.from({ length: ROWS }, (_, i) => `a${i % 100}`); +const b = Array.from({ length: ROWS }, (_, i) => i % 1000); +const tuples: [string, number][] = a.map((v, i) => [v, b[i] as number]); +const mi = new MultiIndex({ tuples }); + +for (let i = 0; i < WARMUP; i++) mi.toList(); +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) mi.toList(); +const total = performance.now() - start; +console.log( + JSON.stringify({ + function: "multi_index_to_list", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_register_option.ts b/benchmarks/tsb/bench_register_option.ts new file mode 100644 index 00000000..38b2157f --- /dev/null +++ b/benchmarks/tsb/bench_register_option.ts @@ -0,0 +1,61 @@ +/** + * Benchmark: registerOption — register custom options with the tsb options system. + * + * Mirrors pandas `pd.core.config.register_option` which allows users to + * register custom options with validators and defaults. + * + * Covers: + * - registerOption(key, default, doc) → register without validator + * - registerOption(key, default, doc, validator) → register with validator + * - getOption / setOption / resetOption on custom keys + * + * Outputs JSON: {"function": "register_option", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { registerOption, getOption, setOption, resetOption } from "../../src/index.ts"; + +const WARMUP = 5; +const ITERATIONS = 1_000; + +// Register options outside the loop (registration is one-time setup) +// Use unique keys per run to avoid conflicts with repeated registrations. +let keyCounter = 0; + +function registerAndExercise(): void { + const key = `bench.custom_${keyCounter++}`; + registerOption(key, 42, "A custom numeric option for benchmarking."); + getOption(key); + setOption(key, 99); + resetOption(key); +} + +function registerWithValidator(): void { + const key = `bench.validated_${keyCounter++}`; + registerOption(key, 10, "A validated numeric option.", (val) => { + if (typeof val !== "number" || val < 0) return "must be a non-negative number"; + return undefined; + }); + setOption(key, 50); + resetOption(key); +} + +// Warm-up +for (let i = 0; i < WARMUP; i++) { + registerAndExercise(); + registerWithValidator(); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + registerAndExercise(); + registerWithValidator(); +} +const total_ms = performance.now() - start; + +console.log( + JSON.stringify({ + function: "register_option", + mean_ms: total_ms / ITERATIONS, + iterations: ITERATIONS, + total_ms: total_ms, + }), +); diff --git a/benchmarks/tsb/bench_string_array_str_ops.ts b/benchmarks/tsb/bench_string_array_str_ops.ts new file mode 100644 index 00000000..36857bcc --- /dev/null +++ b/benchmarks/tsb/bench_string_array_str_ops.ts @@ -0,0 +1,48 @@ +/** + * Benchmark: StringArray additional string operations — + * lstrip, rstrip, startswith, endswith, replace, zfill + * on a 100k-element nullable StringArray (~10 % nulls). + * + * These methods complement bench_string_array.ts (which covers + * upper/lower/strip/contains/len/fillna) with the remaining + * StringArray string utilities. + * + * Outputs JSON: {"function": "string_array_str_ops", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { arrays } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 3; +const ITERATIONS = 50; + +const WORDS = [" hello world ", " foo bar ", "baz qux ", " quux", "corge", "grault ", "garply"]; + +const raw: (string | null)[] = Array.from({ length: N }, (_, i) => + i % 10 === 0 ? null : WORDS[i % WORDS.length], +); + +const a = arrays.StringArray.from(raw); + +function run(): void { + a.lstrip(); + a.rstrip(); + a.startswith(" he"); + a.endswith("ld "); + a.replace("hello", "hi"); + a.zfill(12); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "string_array_str_ops", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_to_dict_series_orient.ts b/benchmarks/tsb/bench_to_dict_series_orient.ts new file mode 100644 index 00000000..038c7d0d --- /dev/null +++ b/benchmarks/tsb/bench_to_dict_series_orient.ts @@ -0,0 +1,41 @@ +/** + * Benchmark: toDictOriented with "series" orient — converts each DataFrame + * column to a Series, producing Record>. + * + * Mirrors pandas DataFrame.to_dict(orient="series") which returns a dict of + * {column_name: Series} pairs. + * + * Outputs JSON: {"function": "to_dict_series_orient", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { DataFrame, toDictOriented } from "../../src/index.js"; + +const ROWS = 10_000; +const WARMUP = 5; +const ITERATIONS = 30; + +const df = DataFrame.fromColumns({ + id: Array.from({ length: ROWS }, (_, i) => i), + value: Array.from({ length: ROWS }, (_, i) => i * 1.5), + label: Array.from({ length: ROWS }, (_, i) => `item_${i % 100}`), + score: Array.from({ length: ROWS }, (_, i) => Math.sin(i * 0.01) * 100), + flag: Array.from({ length: ROWS }, (_, i) => i % 2 === 0), +}); + +for (let i = 0; i < WARMUP; i++) { + toDictOriented(df, "series"); +} + +const t0 = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + toDictOriented(df, "series"); +} +const total = performance.now() - t0; + +console.log( + JSON.stringify({ + function: "to_dict_series_orient", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_wasm_agg_ops.ts b/benchmarks/tsb/bench_wasm_agg_ops.ts new file mode 100644 index 00000000..2b615e65 --- /dev/null +++ b/benchmarks/tsb/bench_wasm_agg_ops.ts @@ -0,0 +1,60 @@ +/** + * Benchmark: WASM-accelerated aggregate operations — sumF64Accelerated, meanF64Accelerated, + * minF64Accelerated, maxF64Accelerated, varF64Accelerated, stdF64Accelerated, medianF64Accelerated + * plus rolling and expanding variants on a 100k-element float64 array. + * + * Mirrors numpy aggregate functions (np.sum, np.mean, np.min, np.max, np.var, np.std, np.median) + * and pandas rolling/expanding window ops. + * + * Outputs JSON: {"function": "wasm_agg_ops", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { + sumF64Accelerated, + meanF64Accelerated, + minF64Accelerated, + maxF64Accelerated, + varF64Accelerated, + stdF64Accelerated, + medianF64Accelerated, + rollingSumF64Accelerated, + rollingMeanF64Accelerated, + expandingSumF64Accelerated, + expandingMeanF64Accelerated, +} from "../../src/wasm/index.ts"; + +const SIZE = 100_000; +const WINDOW = 50; +const MIN_PERIODS = 1; +const WARMUP = 3; +const ITERATIONS = 20; + +const data: number[] = Array.from({ length: SIZE }, (_, i) => Math.sin(i * 0.001) * 1000); + +function run(): void { + sumF64Accelerated(data); + meanF64Accelerated(data); + minF64Accelerated(data); + maxF64Accelerated(data); + varF64Accelerated(data); + stdF64Accelerated(data); + medianF64Accelerated(data); + rollingSumF64Accelerated(data, WINDOW, MIN_PERIODS); + rollingMeanF64Accelerated(data, WINDOW, MIN_PERIODS); + expandingSumF64Accelerated(data, MIN_PERIODS); + expandingMeanF64Accelerated(data, MIN_PERIODS); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "wasm_agg_ops", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_wasm_rolling_stats.ts b/benchmarks/tsb/bench_wasm_rolling_stats.ts new file mode 100644 index 00000000..015326c3 --- /dev/null +++ b/benchmarks/tsb/bench_wasm_rolling_stats.ts @@ -0,0 +1,66 @@ +/** + * Benchmark: WASM-accelerated rolling and expanding statistics — + * rollingMinF64Accelerated, rollingMaxF64Accelerated, rollingVarF64Accelerated, + * rollingStdF64Accelerated, rollingMedianF64Accelerated, + * expandingMinF64Accelerated, expandingMaxF64Accelerated, + * expandingVarF64Accelerated, expandingStdF64Accelerated, + * expandingMedianF64Accelerated on a 100k-element float64 array. + * + * Mirrors pandas Series.rolling() and Series.expanding() with min/max/var/std/median. + * + * Outputs JSON: {"function": "wasm_rolling_stats", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { + rollingMinF64Accelerated, + rollingMaxF64Accelerated, + rollingVarF64Accelerated, + rollingStdF64Accelerated, + rollingMedianF64Accelerated, + expandingMinF64Accelerated, + expandingMaxF64Accelerated, + expandingVarF64Accelerated, + expandingStdF64Accelerated, + expandingMedianF64Accelerated, +} from "../../src/wasm/index.ts"; + +const SIZE = 100_000; +const WINDOW = 50; +const MIN_PERIODS = 1; +const WARMUP = 3; +const ITERATIONS = 20; + +// Deterministic float64 data +const data = new Float64Array(SIZE); +for (let i = 0; i < SIZE; i++) { + data[i] = Math.sin(i * 0.001) * 100 + Math.cos(i * 0.003) * 50; +} + +function runOnce(): void { + rollingMinF64Accelerated(data, WINDOW, MIN_PERIODS); + rollingMaxF64Accelerated(data, WINDOW, MIN_PERIODS); + rollingVarF64Accelerated(data, WINDOW, MIN_PERIODS); + rollingStdF64Accelerated(data, WINDOW, MIN_PERIODS); + rollingMedianF64Accelerated(data, WINDOW, MIN_PERIODS); + expandingMinF64Accelerated(data, MIN_PERIODS); + expandingMaxF64Accelerated(data, MIN_PERIODS); + expandingVarF64Accelerated(data, MIN_PERIODS); + expandingStdF64Accelerated(data, MIN_PERIODS); + expandingMedianF64Accelerated(data, MIN_PERIODS); +} + +// Warm-up +for (let i = 0; i < WARMUP; i++) runOnce(); + +// Measured iterations +const t0 = performance.now(); +for (let i = 0; i < ITERATIONS; i++) runOnce(); +const total_ms = performance.now() - t0; + +console.log( + JSON.stringify({ + function: "wasm_rolling_stats", + mean_ms: total_ms / ITERATIONS, + iterations: ITERATIONS, + total_ms, + }), +);