The default splice implementations used 8 KiB buffers. In the
asynchronous implementation, mixed native-async and worker-backed
streams therefore alternated between reading on the main context and
dispatching each small write through the worker pool.
Use 256 KiB buffers for both synchronous and asynchronous splices to
amortize syscall and dispatch overhead. Allocate the synchronous
buffer on the heap to avoid growing the stack.
Use two buffers for asynchronous splices so the next input read can
run while the previous output write drains. Keep at most one operation
outstanding on each stream to preserve their concurrency guarantees
and provide bounded backpressure.
Stop scheduling new operations after the first error, but wait for any
overlapping callback before completing the splice. This keeps task
lifetimes safe and preserves partial-write, cancellation, close, and
error-reporting semantics.
In a local 1 GiB libsoup download benchmark, this reduced asynchronous
splice time from 3.09 seconds to 0.44 seconds.
Add deterministic tests for synchronous bulk copies, asynchronous
buffer overlap, ordering, partial writes, concurrent read and write
failures, cancellation during overlap, and all close-flag
combinations.
This path function is deprecated because it doesn't work properly if you
are using paths. glib-mkenums is using it "properly" in the sense that
it is not passing it any paths, but on the other hand, passing non-paths
to a path function is pretty silly. You can see why the function is
deprecated.
Embarassingly, my replacement implementation is AI-generated, although I
have simplified it to be less robust than the AI proposed. Why did I
intentionally make the function less robust? Because it's only used in
one place, and doesn't need to be able to handle any possible number of
arguments.
timer_perturb was a static int lazily initialized inside
g_timeout_set_expiration() without any synchronisation, so creating
timeout sources from multiple threads at once was racy.
Use g_once_init_enter()/g_once_init_leave() to init it exactly once,
storing the value plus one since g_once_init_leave() treats 0 as
"not initialized".
WARNING: ThreadSanitizer: data race (pid=6107)
Write of size 4 at 0x7fb009fcdb74 by thread T40:
#0 g_timeout_set_expiration ../glib/gmain.c:5447
#1 timeout_source_new ../glib/gmain.c:5524
#2 g_timeout_source_new_seconds ../glib/gmain.c:5594
#3 source_create_unref_thread_func ../glib/tests/mainloop.c:2810
Previous read of size 4 at 0x7fb009fcdb74 by thread T41:
#0 g_timeout_set_expiration ../glib/gmain.c:5436
#1 timeout_source_new ../glib/gmain.c:5524
#2 g_timeout_source_new_seconds ../glib/gmain.c:5594
#3 source_create_unref_thread_func ../glib/tests/mainloop.c:2810
Location is global 'timer_perturb.7' of size 4
SUMMARY: ThreadSanitizer: data race ../glib/gmain.c:5447 in
g_timeout_set_expiration
g_source_attach_unlocked() was setting source->context without holding
the source_destroy_lock, while source_dup_main_context() reads it under
that lock, so a source being attached (along with its child sources)
could race with another thread cancelling it and querying its context.
Hold the source_destroy_lock as a writer around the attach, in both
g_source_attach() and g_source_add_child_source(), following the
documented lock order (source_destroy_lock, then context lock).
WARNING: ThreadSanitizer: data race (pid=18493)
Read of size 8 at 0x722000009c20 by main thread:
#0 source_dup_main_context ../glib/gmain.c:1399
#1 g_source_update_ready_time_internal ../glib/gmain.c:2047
#2 g_source_set_ready_time ../glib/gmain.c:2133
#3 cancellable_source_cancelled ../gio/gcancellable.c:743
Previous write of size 8 at 0x722000009c20 by thread T3:
#0 g_source_attach_unlocked ../glib/gmain.c:1244
#1 g_source_attach_unlocked ../glib/gmain.c:1268
#2 g_source_attach ../glib/gmain.c:1324
#3 g_task_attach_source ../gio/gtask.c:1807
SUMMARY: ThreadSanitizer: data race ../glib/gmain.c:1399 in
source_dup_main_context
The completed, had_error, result_set and ever_returned flags were packed
into the same bitfield storage unit, but they are accessed from different
threads when a task running in a thread is completed while the task
function is still running (as happens with return-on-cancel): the
completing thread writes completed and reads had_error, while the task
thread keeps writing result_set and ever_returned.
Since those accesses are not otherwise synchronized, the thread sanitizer
reports them as a data race on the shared byte. Split completed and
had_error into their own storage units, leaving result_set and
ever_returned together as they are both only written by the task thread.
WARNING: ThreadSanitizer: data race (pid=11059)
Read of size 1 at 0x723000002858 by thread T6:
#0 g_task_return_int ../gio/gtask.c:1927
#1 return_on_cancel_atomic_thread ../gio/tests/task.c:1935
#2 g_task_thread_pool_thread ../gio/gtask.c:1585
Previous write of size 1 at 0x723000002858 by main thread:
#0 g_task_return_now ../gio/gtask.c:1368
#1 complete_in_idle_cb ../gio/gtask.c:1377
#2 g_idle_dispatch ../glib/gmain.c:6691
SUMMARY: ThreadSanitizer: data race ../gio/gtask.c:1927 in g_task_return_int
WARNING: ThreadSanitizer: data race (pid=1797072)
Write of size 1 at 0x723000000f9c by thread T10:
#0 g_task_return ../gio/gtask.c:1397
#1 g_task_return_int ../gio/gtask.c:1933
#2 return_on_cancel_atomic_thread ../gio/tests/task.c:1935
Previous read of size 1 at 0x723000000f9c by main thread:
#0 g_task_had_error ../gio/gtask.c:2215
#1 return_on_cancel_atomic_callback ../gio/tests/task.c:1884
#2 g_task_return_now ../gio/gtask.c:1365
SUMMARY: ThreadSanitizer: data race ../gio/gtask.c:1397 in g_task_return
g_test_print_handler_full() keeps a flag tracking whether the previous
g_test_print*() output ended mid-line, so the next chunk knows whether
to prepend the TAP line prefix. It was a single shared static, which
races when the handler is called concurrently from multiple threads:
ThreadSanitizer reports a data race between the read that decides the
prefix (at the start of a chunk) and the write that records the chunk's
trailing newline (at its end).
Give each thread its own copy of the flag via GPrivate. The flag is only
meaningful within a single stream (i.e. within one thread), so per-thread
state keeps the logic correct for the common case and removes the shared
memory that ThreadSanitizer flagged.
The pending_waits list is filled by g_subprocess_wait_async() while
holding the pending_waits_lock, but g_subprocess_exited() was freeing
the list items without holding it, potentially racing with a concurrent
wait.
This makes the thread sanitizer unhappy, so free the list nodes while
holding the lock, but only after the tasks have been returned to their
callers, since that may run arbitrary code we don't want to run while
holding it.
WARNING: ThreadSanitizer: data race (pid=1202215)
Write of size 8 at 0x720400000c00 by thread T1:
#0 free_sized ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:748
#1 g_slist_delete_link ../../glib/glib/gslist.c:498
Previous write of size 8 at 0x720400000c00 by main thread:
#0 malloc ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:670
#1 g_malloc ../../glib/glib/gmem.c:100
#2 emit_end_element ../../glib/glib/gmarkup.c:1077
Thread T1 'gmain' (tid=1202287, running) created by main thread at:
#0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1078
#1 g_system_thread_new ../../glib/glib/gthread-posix.c:762
#2 emit_end_element ../../glib/glib/gmarkup.c:1077
SUMMARY: ThreadSanitizer: data race ../../glib/glib/gslist.c:498 in
g_slist_delete_link
The shift `1 << g_bit_storage (...)` has type `int`, which is then
implicitly converted to `guint` when the macro result is assigned to a
`guint` variable, triggering `-Wsign-conversion`.
Use an unsigned literal so the result is unsigned and no conversion is
needed.
The tool may want to launch a desktop file, and if the launcher tool is
not available yet we fail with:
# gio: Unable to launch application ‘/tmp/tmphl5tyb5u/folder/desktop.entry’:
# Failed to execute child process
# “/builds/GNOME/glib/_build/gio/gio-launch-desktop” (No such file or directory)
---
message: |
Traceback (most recent call last):
File "/builds/GNOME/glib/_build/../gio/tests/gio-tool.py", line 141, in
test_absolute_from_folder
self.launchAndCheck(self.entry, cwd=self.folder)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/builds/GNOME/glib/_build/../gio/tests/gio-tool.py", line 135, in
launchAndCheck
result = self.runTestProgram(["launch", str(entry)], cwd=str(cwd))
File "/builds/GNOME/glib/tests/lib/testprogramrunner.py", line 157, in
runTestProgram
info.check_returncode()
~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib64/python3.13/subprocess.py", line 508, in check_returncode
raise CalledProcessError(self.returncode, self.args, self.stdout,
self.stderr)
subprocess.CalledProcessError:
Command '['/builds/GNOME/glib/_build/gio/gio', 'launch',
'/tmp/tmphl5tyb5u/folder/desktop.entry']'
returned non-zero exit status 1.
Activating default application when ENTER, SPACE and LEFTCLICK keystrokes are pressed.
gnotification-win32: Showing tooltip on notification icon
Tooltip based on string returned by g_get_application_name