From 01716e1912c6ff5ccd2a992c57b97bff749f441d Mon Sep 17 00:00:00 2001 From: Bryan Green Date: Wed, 4 Feb 2026 08:37:32 -0600 Subject: [PATCH v2] Avoid gettext 0.20+ performance bug on Windows. gettext 0.20+ expects Windows locale format ("English_United States") not POSIX format ("en_US"), and has a cache bug where failed lookups cause repeated enumeration of all ~259 system locales on every gettext() call. This makes exception-heavy workloads 5-6x slower. PostgreSQL converts to POSIX format via IsoLocaleName() before setting LC_MESSAGES, which triggers this bug. Setting lc_messages to 'C' or 'POSIX' triggers it too, since these aren't Windows names. Fix by using Windows format for gettext 0.20+, which handles it correctly. Retain POSIX format for 0.19.8 and earlier. Detect version via LIBINTL_VERSION macro. Map "C" and "POSIX" to the current LC_CTYPE locale to avoid the enumeration bug for those special values. Mark IsoLocaleName() with pg_attribute_unused() to suppress compiler warnings when building with gettext 0.20+, as the function becomes unreferenced in that configuration. We should strongly suggest that legacy windows locale format and not POSIX format be used on Windows. The caching bug in gnulib has been fixed and gnu gettext includes the fixed gnulib in version 1.0. Improves 1M exception test from ~180s to ~40s. --- src/backend/utils/adt/pg_locale.c | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index ac324ecaad..4ab6b2703e 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -233,10 +233,38 @@ pg_perm_setlocale(int category, const char *locale) case LC_MESSAGES: envvar = "LC_MESSAGES"; #ifdef WIN32 + + /* + * gettext 0.20+ expects Windows locale names (e.g., + * "English_United States") rather than POSIX names (e.g., + * "en_US"). When given a POSIX name, its get_lcid() function + * enumerates all ~259 system locales looking for a match, fails, + * and due to a cache bug, repeats this enumeration on every + * gettext() call. This causes exception-heavy workloads to be + * 5-6x slower. + * + * For gettext 0.20+, pass the locale through as-is (it's already + * in Windows format from setlocale()). For older versions, convert + * to POSIX/ISO format via IsoLocaleName() since they expect that. + * + * "C" and "POSIX" are not valid Windows locale names and would + * trigger the same enumeration bug, so map them to the current + * LC_CTYPE locale instead. + * + * Note: locale is guaranteed non-NULL and non-empty here, as those + * cases are handled earlier in the LC_MESSAGES block above. + */ +#if defined(LIBINTL_VERSION) && (LIBINTL_VERSION >= 0x001400) + if (strcmp(locale, "C") == 0 || strcmp(locale, "POSIX") == 0) + result = setlocale(LC_CTYPE, NULL); + else + result = (char *) locale; +#else result = IsoLocaleName(locale); if (result == NULL) result = (char *) locale; - elog(DEBUG3, "IsoLocaleName() executed; locale: \"%s\"", result); +#endif + elog(DEBUG3, "LC_MESSAGES locale: \"%s\"", result); #endif /* WIN32 */ break; #endif /* LC_MESSAGES */ @@ -1025,6 +1053,7 @@ get_iso_localename(const char *winlocname) return NULL; } +pg_attribute_unused() static char * IsoLocaleName(const char *winlocname) { -- 2.52.0.windows.1