From b094d5a5885ad757f3b9edffe9ddc8197bef60d5 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 17 Oct 2025 13:04:11 +0200 Subject: [PATCH 1/3] [skip ci] Skip suspicious fpm tests on GH actions & macOS There have been misterious macOS failures with the following error for a long time, which would usually happen 1-2x per nightly run: > The hosted runner lost communication with the server. Anything in your > workflow that terminates the runner process, starves it for CPU/Memory, or > blocks its network access can cause this error. After way too much debugging, it looks like this is caused by the two fpm tests skipped in this commit. When there's a failure, the responsible test will run for at least 120 seconds until the job is eventually killed by GitHub Actions. It's not clear yet why the tests stall. Debugging this is a PITA because the GitHub Actions log gets partially lost for killed jobs. It took an external log server to actually figure out where the job fails. Let's disable the tests first to be sure this actually solves the issue. --- sapi/fpm/tests/bug74083-concurrent-reload.phpt | 1 + sapi/fpm/tests/bug76601-reload-child-signals.phpt | 1 + 2 files changed, 2 insertions(+) diff --git a/sapi/fpm/tests/bug74083-concurrent-reload.phpt b/sapi/fpm/tests/bug74083-concurrent-reload.phpt index ad5d560abae47..5fed01f798b12 100644 --- a/sapi/fpm/tests/bug74083-concurrent-reload.phpt +++ b/sapi/fpm/tests/bug74083-concurrent-reload.phpt @@ -4,6 +4,7 @@ Concurrent reload signals should not kill PHP-FPM master process. (Bug: #74083) --FILE-- --FILE-- Date: Fri, 17 Oct 2025 13:21:49 +0200 Subject: [PATCH 2/3] [skip ci] Fix typo Should've been skip, not flaky... --- sapi/fpm/tests/bug74083-concurrent-reload.phpt | 2 +- sapi/fpm/tests/bug76601-reload-child-signals.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sapi/fpm/tests/bug74083-concurrent-reload.phpt b/sapi/fpm/tests/bug74083-concurrent-reload.phpt index 5fed01f798b12..f37085d65c4c0 100644 --- a/sapi/fpm/tests/bug74083-concurrent-reload.phpt +++ b/sapi/fpm/tests/bug74083-concurrent-reload.phpt @@ -4,7 +4,7 @@ Concurrent reload signals should not kill PHP-FPM master process. (Bug: #74083) --FILE-- --FILE-- Date: Mon, 1 Sep 2025 13:15:35 +0200 Subject: [PATCH 3/3] re-use `sprintf()` optimisation for `printf()` Closes GH-19658 --- UPGRADING | 5 +++++ Zend/Optimizer/dce.c | 7 ++++-- Zend/Optimizer/pass1.c | 10 +++++++++ Zend/zend_compile.c | 50 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/UPGRADING b/UPGRADING index 4b166bb5af463..765b8c36a4ac8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -99,5 +99,10 @@ PHP 8.6 UPGRADE NOTES 14. Performance Improvements ======================================== +- Core: + . `printf()` using only `%s` and `%d` will be compiled into the equivalent + string interpolation, avoiding the overhead of a function call and repeatedly + parsing the format string. + - JSON: . Improve performance of encoding arrays and objects. diff --git a/Zend/Optimizer/dce.c b/Zend/Optimizer/dce.c index 7993778a1257d..41fd0f6c30af4 100644 --- a/Zend/Optimizer/dce.c +++ b/Zend/Optimizer/dce.c @@ -124,6 +124,7 @@ static inline bool may_have_side_effects( case ZEND_FUNC_NUM_ARGS: case ZEND_FUNC_GET_ARGS: case ZEND_ARRAY_KEY_EXISTS: + case ZEND_COPY_TMP: /* No side effects */ return false; case ZEND_FREE: @@ -425,10 +426,12 @@ static bool dce_instr(const context *ctx, zend_op *opline, zend_ssa_op *ssa_op) return false; } - if ((opline->op1_type & (IS_VAR|IS_TMP_VAR))&& !is_var_dead(ctx, ssa_op->op1_use)) { + if ((opline->op1_type & (IS_VAR|IS_TMP_VAR)) && !is_var_dead(ctx, ssa_op->op1_use)) { if (!try_remove_var_def(ctx, ssa_op->op1_use, ssa_op->op1_use_chain, opline)) { if (may_be_refcounted(ssa->var_info[ssa_op->op1_use].type) - && opline->opcode != ZEND_CASE && opline->opcode != ZEND_CASE_STRICT) { + && opline->opcode != ZEND_CASE + && opline->opcode != ZEND_CASE_STRICT + && opline->opcode != ZEND_COPY_TMP) { free_var = ssa_op->op1_use; free_var_type = opline->op1_type; } diff --git a/Zend/Optimizer/pass1.c b/Zend/Optimizer/pass1.c index 09a0ae5fec531..4be966c25d896 100644 --- a/Zend/Optimizer/pass1.c +++ b/Zend/Optimizer/pass1.c @@ -264,6 +264,16 @@ void zend_optimizer_pass1(zend_op_array *op_array, zend_optimizer_ctx *ctx) collect_constants = false; break; } + case ZEND_DO_UCALL: + case ZEND_DO_FCALL: + case ZEND_DO_FCALL_BY_NAME: + case ZEND_FRAMELESS_ICALL_0: + case ZEND_FRAMELESS_ICALL_1: + case ZEND_FRAMELESS_ICALL_2: + case ZEND_FRAMELESS_ICALL_3: + /* don't collect constants after any UCALL/FCALL/FRAMELESS ICALL */ + collect_constants = 0; + break; case ZEND_STRLEN: if (opline->op1_type == IS_CONST && zend_optimizer_eval_strlen(&result, &ZEND_OP1_LITERAL(opline)) == SUCCESS) { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d61c2df0a3555..b51a519533c6b 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -28,6 +28,7 @@ #include "zend_API.h" #include "zend_exceptions.h" #include "zend_interfaces.h" +#include "zend_types.h" #include "zend_virtual_cwd.h" #include "zend_multibyte.h" #include "zend_language_scanner.h" @@ -4968,6 +4969,53 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args) return SUCCESS; } +static zend_result zend_compile_func_printf(znode *result, zend_ast_list *args) /* {{{ */ +{ + /* Special case: printf with a single constant string argument and no format specifiers. + * In this case, just emit ECHO and return the string length if needed. */ + if (args->children == 1) { + zend_eval_const_expr(&args->child[0]); + if (args->child[0]->kind != ZEND_AST_ZVAL) { + return FAILURE; + } + zval *format_string = zend_ast_get_zval(args->child[0]); + if (Z_TYPE_P(format_string) != IS_STRING) { + return FAILURE; + } + /* Check if there are any format specifiers */ + if (!memchr(Z_STRVAL_P(format_string), '%', Z_STRLEN_P(format_string))) { + /* No format specifiers - just emit ECHO and return string length */ + znode format_node; + zend_compile_expr(&format_node, args->child[0]); + zend_emit_op(NULL, ZEND_ECHO, &format_node, NULL); + + /* Return the string length as a constant if the result is used */ + result->op_type = IS_CONST; + ZVAL_LONG(&result->u.constant, Z_STRLEN_P(format_string)); + return SUCCESS; + } + } + + /* Fall back to sprintf optimization for format strings with specifiers */ + znode rope_result; + if (zend_compile_func_sprintf(&rope_result, args) != SUCCESS) { + return FAILURE; + } + + /* printf() returns the amount of bytes written, so just an ECHO of the + * resulting sprintf() optimisation might not be enough. At this early + * stage we can't detect if the result is actually used, so we just emit + * the opcodes and let them be cleaned up by the dead code elimination + * pass in the Zend Optimizer if the result of the printf() is in fact + * unused */ + znode copy; + zend_emit_op_tmp(©, ZEND_COPY_TMP, &rope_result, NULL); + zend_emit_op(NULL, ZEND_ECHO, &rope_result, NULL); + zend_emit_op_tmp(result, ZEND_STRLEN, ©, NULL); + + return SUCCESS; +} + static zend_result zend_compile_func_clone(znode *result, zend_ast_list *args) { znode arg_node; @@ -5050,6 +5098,8 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string * return zend_compile_func_array_key_exists(result, args); } else if (zend_string_equals_literal(lcname, "sprintf")) { return zend_compile_func_sprintf(result, args); + } else if (zend_string_equals_literal(lcname, "printf")) { + return zend_compile_func_printf(result, args); } else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) { return zend_compile_func_clone(result, args); } else {