diff --git a/JetStreamDriver.js b/JetStreamDriver.js index f248e11e..225e26cd 100644 --- a/JetStreamDriver.js +++ b/JetStreamDriver.js @@ -1323,7 +1323,7 @@ class AsyncBenchmark extends DefaultBenchmark { get runnerCode() { return ` async function doRun() { - const benchmark = new Benchmark(${this.iterations}); + const benchmark = new Benchmark(${JSON.stringify(this.benchmarkArguments)}); await benchmark.init?.(); const results = []; const benchmarkName = "${this.name}"; @@ -2534,6 +2534,37 @@ let BENCHMARKS = [ ]; +const INTL_TESTS = [ + "DateTimeFormat", + "ListFormat", + "RelativeTimeFormat", + "NumberFormat", + "PluralRules", +]; +const INTL_BENCHMARKS = []; +for (const test of INTL_TESTS) { + const benchmark = new AsyncBenchmark({ + name: `${test}-intl`, + files: [ + "./intl/src/helper.js", + `./intl/src/${test}.js`, + "./intl/benchmark.js", + ], + iterations: 2, + worstCaseCount: 1, + deterministicRandom: true, + tags: ["Javascript", "intl"], + }); + INTL_BENCHMARKS.push(benchmark); +} +BENCHMARKS.push( + new GroupedBenchmark({ + name: "intl", + tags: ["Javascript", "intl"], + }, INTL_BENCHMARKS)); + + + // SunSpider tests const SUNSPIDER_TESTS = [ "3d-cube", diff --git a/intl/benchmark.js b/intl/benchmark.js new file mode 100644 index 00000000..8c835952 --- /dev/null +++ b/intl/benchmark.js @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +class Benchmark { + iterationCount; + verbose; + lastResult; + totalLength = 0; + expectedMinLength = 0; + + constructor({ iterationCount, verbose = false } = {}) { + this.iterationCount = iterationCount; + this.verbose = verbose; + } + + runIteration() { + // See implementations in src/. + const { lastResult, totalLength, expectedMinLength } = runTest( + this.verbose + ); + this.lastResult = lastResult; + this.totalLength += totalLength; + this.expectedMinLength = expectedMinLength; + } + + validate() { + const expectedMinTotalLength = this.expectedMinLength * this.iterationCount; + console.assert( + this.totalLength >= expectedMinTotalLength, + `Invalid totalLength = ${this.totalLength}, expected >= ${expectedMinTotalLength}` + ); + } +} diff --git a/intl/src/DateTimeFormat.js b/intl/src/DateTimeFormat.js new file mode 100644 index 00000000..ee981bf2 --- /dev/null +++ b/intl/src/DateTimeFormat.js @@ -0,0 +1,77 @@ +function generateRandomDates(count) { + const firstDate = new Date(1800, 11, 5, 13, 6); + let currentTimeStamp = firstDate.getTime(); + const dates = []; + + for (let i = 0; i < count; i++) { + dates.push(new Date(currentTimeStamp)); + currentTimeStamp += 1234569; + } + return dates; +} + +const DATE_STYLE_OPTIONS = ["full", "long", "medium", "short"]; +const TIME_STYLE_OPTIONS = ["full", "long", "medium", "short"]; + +function* dateTimeFormatOptions() { + for (const locale of LOCALES) { + for (const dateStyle of DATE_STYLE_OPTIONS) { + for (const timeStyle of TIME_STYLE_OPTIONS) { + yield { locale, dateStyle, timeStyle }; + } + } + } +} + +function runTest(verbose = false) { + let totalLength = 0; + let lastFormatResult; + let lastFormatPartResult; + let lastFormatRangeResult; + const dates = generateRandomDates(100); + let dateIndex = 0; + + const FORMAT_COUNT = 17; + const FORMAT_RANGE_COUNT = 7; + for (const { locale, dateStyle, timeStyle } of shuffleOptions( + dateTimeFormatOptions + )) { + const options = { dateStyle, timeStyle }; + if (verbose) { + console.log(locale, JSON.stringify(options)); + } + const formatter = new Intl.DateTimeFormat(locale, options); + for (let i = 0; i < FORMAT_COUNT; i++) { + let date = dates[dateIndex % dates.length]; + lastFormatResult = formatter.format(date); + totalLength += lastFormatResult.length; + if (verbose) { + console.log(date, lastFormatResult); + } + dateIndex++; + + date = dates[dateIndex % dates.length]; + lastFormatPartResult = formatter.formatToParts(date); + for (const part of lastFormatPartResult) { + totalLength += part.value.length; + } + dateIndex++; + } + let dateRangeStart = dates[0]; + for (let i = 0; i < FORMAT_RANGE_COUNT; i++) { + const date = dates[dateIndex % dates.length]; + if (dateRangeStart < date) { + lastFormatRangeResult = formatter.formatRange(dateRangeStart, date); + if (verbose) { + console.log(dateRangeStart, date, lastFormatRangeResult); + } + } + dateRangeStart = date; + } + } + return { + lastResult: lastFormatResult + lastFormatRangeResult, + totalLength, + expectedMinLength: 438_000, + }; +} diff --git a/intl/src/ListFormat.js b/intl/src/ListFormat.js new file mode 100644 index 00000000..9849853c --- /dev/null +++ b/intl/src/ListFormat.js @@ -0,0 +1,47 @@ +const LISTS = [ + ["One"], + ["1", "2"], + ["Motorcycle", "Bus", "Car"], + LOCALES, + new Array(100).fill(9).map((_, index) => index.toString()), +]; + +function* listOptions() { + const styleOptions = ["long", "short", "narrow"]; + const typeOptions = ["conjunction", "disjunction", "unit"]; + for (const locale of LOCALES) { + for (const style of styleOptions) { + for (const type of typeOptions) { + yield { locale, style, type }; + } + } + } +} + +function runTest(verbose = false) { + let lastResult; + let totalLength = 0; + const LIST_FORMAT_COUNT = 10; + let listIndex = 0; + for (const { locale, style, type } of shuffleOptions(listOptions)) { + const options = { style, type }; + if (verbose) { + console.log(locale, JSON.stringify(options)); + } + const formatter = new Intl.ListFormat(locale, options); + for (let i = 0; i < LIST_FORMAT_COUNT; i++) { + const list = LISTS[listIndex % LISTS.length]; + listIndex++; + lastResult = formatter.format(list); + totalLength += lastResult.length; + const formatPartsResult = formatter.formatToParts(list); + if (verbose) { + console.log(value, lastResult); + } + for (const part of formatPartsResult) { + totalLength += part.value.length; + } + } + } + return { lastResult, totalLength, expectedMinLength: 506_000 }; +} diff --git a/intl/src/NumberFormat.js b/intl/src/NumberFormat.js new file mode 100644 index 00000000..6a847214 --- /dev/null +++ b/intl/src/NumberFormat.js @@ -0,0 +1,96 @@ +const CURRENCIES = ["USD", "EUR", "JPY", "INR", "NGN"]; + +const NUMBER_UNITS = [ + "acre", + "bit", + "byte", + "celsius", + "centimeter", + "day", + "degree", + "fahrenheit", + "fluid-ounce", + "foot", + "gallon", + "gigabit", + "gigabyte", + "gram", + "hectare", + "hour", + "inch", + "kilobit", + "kilobyte", + "kilogram", + "kilometer", + "liter", + "megabit", + "megabyte", + "meter", + "microsecond", + "mile", + "mile-scandinavian", + "milliliter", + "millimeter", + "millisecond", + "minute", + "month", + "nanosecond", + "ounce", + "percent", + "petabyte", + "pound", + "second", + "stone", + "terabit", + "terabyte", + "week", + "yard", + "year", +]; + +function* numberFormatOptions() { + const currencyDisplayOptions = ["symbol", "narrowSymbol", "code", "name"]; + const unitDisplayOptions = ["short", "long", "narrow"]; + + for (const locale of LOCALES) { + for (const currency of CURRENCIES) { + for (const currencyDisplay of currencyDisplayOptions) { + yield { locale, style: "currency", currency, currencyDisplay }; + } + } + for (const unit of NUMBER_UNITS.slice(0, 20)) { + for (const unitDisplay of unitDisplayOptions) { + yield { locale, style: "unit", unit, unitDisplay }; + } + } + yield { locale, style: "decimal" }; + yield { locale, style: "percent" }; + } +} + +function runTest(verbose = false) { + let lastResult; + let totalLength = 0; + const NUMBER_FORMAT_COUNT = 10; + let counter = 1; + for (const options of shuffleOptions(numberFormatOptions).slice(0, 200)) { + const formatter = new Intl.NumberFormat(options.locale, options); + if (verbose) { + console.log(options.locale, JSON.stringify(options)); + } + for (let i = 0; i < NUMBER_FORMAT_COUNT; i++) { + counter += 599; + const value = counter % 10_000; + lastResult = formatter.format(value); + if (verbose) { + console.log(value, lastResult); + } + totalLength += lastResult.length; + const formatPartsResult = formatter.formatToParts(value); + for (const part of formatPartsResult) { + totalLength += part.value.length; + } + } + } + return { lastResult, totalLength, expectedMinLength: 40_000 }; +} diff --git a/intl/src/PluralRules.js b/intl/src/PluralRules.js new file mode 100644 index 00000000..944aa390 --- /dev/null +++ b/intl/src/PluralRules.js @@ -0,0 +1,38 @@ +function* pluralRulesOptions() { + const typeOptions = ["cardinal", "ordinal"]; + for (const locale of LOCALES) { + for (const type of typeOptions) { + yield { locale, type }; + } + } +} + +function runTest(verbose = false) { + let lastResult; + let totalLength = 0; + const PLURAL_RULES_COUNT = 1000; + for (const { locale, type } of shuffleOptions(pluralRulesOptions)) { + if (verbose) { + console.log(locale, type); + } + const formatter = new Intl.PluralRules(locale, { type }); + let i = 0; + for (let value = 0; value < 4; value++) { + lastResult = formatter.select(value); + totalLength += lastResult.length; + if (verbose) { + console.log(value, lastResult); + } + i++; + } + for (; i < PLURAL_RULES_COUNT; i++) { + const value = Math.floor(Math.random() * 1000); + lastResult = formatter.select(value); + totalLength += lastResult.length; + if (verbose) { + console.log(value, lastResult); + } + } + } + return { lastResult, totalLength, expectedMinLength: 244_000 }; +} diff --git a/intl/src/RelativeTimeFormat.js b/intl/src/RelativeTimeFormat.js new file mode 100644 index 00000000..e0d0394a --- /dev/null +++ b/intl/src/RelativeTimeFormat.js @@ -0,0 +1,53 @@ +const UNITS = [ + "year", + "quarter", + "month", + "week", + "day", + "hour", + "minute", + "second", +]; + +function* relativeTimeFormatOptions() { + const styleOptions = ["long", "short", "narrow"]; + const numericOptions = ["always", "auto"]; + for (const locale of LOCALES) { + for (const style of styleOptions) { + for (const numeric of numericOptions) { + yield { locale, style, numeric }; + } + } + } +} + +function runTest(verbose = false) { + let lastResult; + let totalLength = 0; + const RELATIVE_TIME_FORMAT_COUNT = 100; + let unitIndex = 0; + for (const { locale, style, numeric } of shuffleOptions( + relativeTimeFormatOptions + )) { + const options = { style, numeric }; + if (verbose) { + console.log(locale, JSON.stringify(options)); + } + const formatter = new Intl.RelativeTimeFormat(locale, options); + for (let i = 0; i < RELATIVE_TIME_FORMAT_COUNT; i++) { + const unit = UNITS[unitIndex % UNITS.length]; + unitIndex++; + const value = Math.random() * 100 - 50; + lastResult = formatter.format(value, unit); + if (verbose) { + console.log(value, unit, lastResult); + } + totalLength += lastResult.length; + const formatPartsResult = formatter.formatToParts(value, unit); + for (const part of formatPartsResult) { + totalLength += part.value.length; + } + } + } + return { lastResult, totalLength, expectedMinLength: 432_000 }; +} diff --git a/intl/src/helper.js b/intl/src/helper.js new file mode 100644 index 00000000..9a173f68 --- /dev/null +++ b/intl/src/helper.js @@ -0,0 +1,39 @@ +function shuffleOptions(optionsGenerator) { + const options = Array.from(optionsGenerator()); + for (let i = options.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [options[i], options[j]] = [options[j], options[i]]; + } + return options; +} + +const LOCALES = [ + "ar-SA", + "zh-CN", + "zh-TW", + "da-DK", + "en-US", + "en-GB", + "en-CA", + "en-AU", + "fr-FR", + "fr-CA", + "de-DE", + "hi-IN", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "pt-PT", + "ru-RU", + "es-ES", + "es-MX", + "sw-KE", + "sv-SE", + "th-TH", + "tr-TR", + "vi-VN", +]; + +globalThis.console ??= {}; +console.log ??= (...args) => print(...args);