1/*
2 Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
3
4 This software is provided 'as-is', without any express or implied
5 warranty. In no event will the authors be held liable for any damages
6 arising from the use of this software.
7
8 Permission is granted to anyone to use this software for any purpose,
9 including commercial applications, and to alter it and redistribute it
10 freely.
11*/
12
13/* Simple test of the SDL semaphore code */
14
15#include <signal.h>
16
17#include <SDL3/SDL.h>
18#include <SDL3/SDL_main.h>
19#include <SDL3/SDL_test.h>
20
21#define NUM_THREADS 10
22/* This value should be smaller than the maximum count of the */
23/* semaphore implementation: */
24#define NUM_OVERHEAD_OPS 10000
25#define NUM_OVERHEAD_OPS_MULT 10
26
27static SDL_Semaphore *sem;
28static int alive;
29
30typedef struct Thread_State
31{
32 SDL_Thread *thread;
33 int number;
34 bool flag;
35 int loop_count;
36 int content_count;
37} Thread_State;
38
39static void log_usage(char *progname, SDLTest_CommonState *state) {
40 static const char *options[] = { "[--no-threads]", "init_value", NULL };
41 SDLTest_CommonLogUsage(state, progname, options);
42}
43
44static void
45killed(int sig)
46{
47 alive = 0;
48}
49
50static int SDLCALL
51ThreadFuncRealWorld(void *data)
52{
53 Thread_State *state = (Thread_State *)data;
54 while (alive) {
55 SDL_WaitSemaphore(sem);
56 SDL_Log("Thread number %d has got the semaphore (value = %" SDL_PRIu32 ")!\n",
57 state->number, SDL_GetSemaphoreValue(sem));
58 SDL_Delay(200);
59 SDL_SignalSemaphore(sem);
60 SDL_Log("Thread number %d has released the semaphore (value = %" SDL_PRIu32 ")!\n",
61 state->number, SDL_GetSemaphoreValue(sem));
62 ++state->loop_count;
63 SDL_Delay(1); /* For the scheduler */
64 }
65 SDL_Log("Thread number %d exiting.\n", state->number);
66 return 0;
67}
68
69static void
70TestRealWorld(int init_sem)
71{
72 Thread_State thread_states[NUM_THREADS] = { { 0 } };
73 int i;
74 int loop_count;
75
76 sem = SDL_CreateSemaphore(init_sem);
77
78 SDL_Log("Running %d threads, semaphore value = %d\n", NUM_THREADS,
79 init_sem);
80 alive = 1;
81 /* Create all the threads */
82 for (i = 0; i < NUM_THREADS; ++i) {
83 char name[64];
84 (void)SDL_snprintf(name, sizeof(name), "Thread%u", (unsigned int)i);
85 thread_states[i].number = i;
86 thread_states[i].thread = SDL_CreateThread(ThreadFuncRealWorld, name, (void *)&thread_states[i]);
87 }
88
89 /* Wait 10 seconds */
90 SDL_Delay(10 * 1000);
91
92 /* Wait for all threads to finish */
93 SDL_Log("Waiting for threads to finish\n");
94 alive = 0;
95 loop_count = 0;
96 for (i = 0; i < NUM_THREADS; ++i) {
97 SDL_WaitThread(thread_states[i].thread, NULL);
98 loop_count += thread_states[i].loop_count;
99 }
100 SDL_Log("Finished waiting for threads, ran %d loops in total\n\n", loop_count);
101
102 SDL_DestroySemaphore(sem);
103}
104
105static void
106TestWaitTimeout(void)
107{
108 Uint64 start_ticks;
109 Uint64 end_ticks;
110 Uint64 duration;
111 bool result;
112
113 sem = SDL_CreateSemaphore(0);
114 SDL_Log("Waiting 2 seconds on semaphore\n");
115
116 start_ticks = SDL_GetTicks();
117 result = SDL_WaitSemaphoreTimeout(sem, 2000);
118 end_ticks = SDL_GetTicks();
119
120 duration = end_ticks - start_ticks;
121
122 /* Accept a little offset in the effective wait */
123 SDL_Log("Wait took %" SDL_PRIu64 " milliseconds\n\n", duration);
124 SDL_assert(duration > 1900 && duration < 2050);
125
126 /* Check to make sure the return value indicates timed out */
127 if (result) {
128 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_WaitSemaphoreTimeout returned: %d; expected: false\n\n", result);
129 }
130
131 SDL_DestroySemaphore(sem);
132}
133
134static void
135TestOverheadUncontended(void)
136{
137 Uint64 start_ticks;
138 Uint64 end_ticks;
139 Uint64 duration;
140 int i, j;
141
142 sem = SDL_CreateSemaphore(0);
143 SDL_Log("Doing %d uncontended Post/Wait operations on semaphore\n", NUM_OVERHEAD_OPS * NUM_OVERHEAD_OPS_MULT);
144
145 start_ticks = SDL_GetTicks();
146 for (i = 0; i < NUM_OVERHEAD_OPS_MULT; i++) {
147 for (j = 0; j < NUM_OVERHEAD_OPS; j++) {
148 SDL_SignalSemaphore(sem);
149 }
150 for (j = 0; j < NUM_OVERHEAD_OPS; j++) {
151 SDL_WaitSemaphore(sem);
152 }
153 }
154 end_ticks = SDL_GetTicks();
155
156 duration = end_ticks - start_ticks;
157 SDL_Log("Took %" SDL_PRIu64 " milliseconds\n\n", duration);
158
159 SDL_DestroySemaphore(sem);
160}
161
162static int SDLCALL
163ThreadFuncOverheadContended(void *data)
164{
165 Thread_State *state = (Thread_State *)data;
166
167 if (state->flag) {
168 while (alive) {
169 if (!SDL_TryWaitSemaphore(sem)) {
170 ++state->content_count;
171 }
172 ++state->loop_count;
173 }
174 } else {
175 while (alive) {
176 /* Timeout needed to allow check on alive flag */
177 if (!SDL_WaitSemaphoreTimeout(sem, 50)) {
178 ++state->content_count;
179 }
180 ++state->loop_count;
181 }
182 }
183 return 0;
184}
185
186static void
187TestOverheadContended(bool try_wait)
188{
189 Uint64 start_ticks;
190 Uint64 end_ticks;
191 Uint64 duration;
192 Thread_State thread_states[NUM_THREADS] = { { 0 } };
193 char textBuffer[1024];
194 int loop_count;
195 int content_count;
196 int i, j;
197 size_t len;
198
199 sem = SDL_CreateSemaphore(0);
200 SDL_Log("Doing %d contended %s operations on semaphore using %d threads\n",
201 NUM_OVERHEAD_OPS * NUM_OVERHEAD_OPS_MULT, try_wait ? "Post/TryWait" : "Post/WaitTimeout", NUM_THREADS);
202 alive = 1;
203 /* Create multiple threads to starve the semaphore and cause contention */
204 for (i = 0; i < NUM_THREADS; ++i) {
205 char name[64];
206 (void)SDL_snprintf(name, sizeof(name), "Thread%u", (unsigned int)i);
207 thread_states[i].flag = try_wait;
208 thread_states[i].thread = SDL_CreateThread(ThreadFuncOverheadContended, name, (void *)&thread_states[i]);
209 }
210
211 start_ticks = SDL_GetTicks();
212 for (i = 0; i < NUM_OVERHEAD_OPS_MULT; i++) {
213 for (j = 0; j < NUM_OVERHEAD_OPS; j++) {
214 SDL_SignalSemaphore(sem);
215 }
216 /* Make sure threads consumed everything */
217 while (SDL_GetSemaphoreValue(sem)) {
218 /* Friendlier with cooperative threading models */
219 SDL_DelayNS(1);
220 }
221 }
222 end_ticks = SDL_GetTicks();
223
224 alive = 0;
225 loop_count = 0;
226 content_count = 0;
227 for (i = 0; i < NUM_THREADS; ++i) {
228 SDL_WaitThread(thread_states[i].thread, NULL);
229 loop_count += thread_states[i].loop_count;
230 content_count += thread_states[i].content_count;
231 }
232 SDL_assert_release((loop_count - content_count) == NUM_OVERHEAD_OPS * NUM_OVERHEAD_OPS_MULT);
233
234 duration = end_ticks - start_ticks;
235 SDL_Log("Took %" SDL_PRIu64 " milliseconds, threads %s %d out of %d times in total (%.2f%%)\n",
236 duration, try_wait ? "where contended" : "timed out", content_count,
237 loop_count, ((float)content_count * 100) / loop_count);
238 /* Print how many semaphores where consumed per thread */
239 (void)SDL_snprintf(textBuffer, sizeof(textBuffer), "{ ");
240 for (i = 0; i < NUM_THREADS; ++i) {
241 if (i > 0) {
242 len = SDL_strlen(textBuffer);
243 (void)SDL_snprintf(textBuffer + len, sizeof(textBuffer) - len, ", ");
244 }
245 len = SDL_strlen(textBuffer);
246 (void)SDL_snprintf(textBuffer + len, sizeof(textBuffer) - len, "%d", thread_states[i].loop_count - thread_states[i].content_count);
247 }
248 len = SDL_strlen(textBuffer);
249 (void)SDL_snprintf(textBuffer + len, sizeof(textBuffer) - len, " }\n");
250 SDL_Log("%s\n", textBuffer);
251
252 SDL_DestroySemaphore(sem);
253}
254
255int main(int argc, char **argv)
256{
257 int arg_count = 0;
258 int i;
259 int init_sem = 0;
260 bool enable_threads = true;
261 SDLTest_CommonState *state;
262
263 /* Initialize test framework */
264 state = SDLTest_CommonCreateState(argv, 0);
265 if (!state) {
266 return 1;
267 }
268
269 /* Parse commandline */
270 for (i = 1; i < argc;) {
271 int consumed;
272
273 consumed = SDLTest_CommonArg(state, i);
274 if (consumed == 0) {
275 consumed = -1;
276 if (SDL_strcasecmp(argv[i], "--no-threads") == 0) {
277 enable_threads = false;
278 consumed = 1;
279 } else if (arg_count == 0) {
280 char *endptr;
281 init_sem = SDL_strtol(argv[i], &endptr, 0);
282 if (endptr != argv[i] && *endptr == '\0') {
283 arg_count++;
284 consumed = 1;
285 }
286 }
287 }
288 if (consumed <= 0) {
289 log_usage(argv[0], state);
290 return 1;
291 }
292
293 i += consumed;
294 }
295
296 if (arg_count != 1) {
297 log_usage(argv[0], state);
298 return 1;
299 }
300
301 /* Load the SDL library */
302 if (!SDL_Init(0)) {
303 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s\n", SDL_GetError());
304 return 1;
305 }
306 (void)signal(SIGTERM, killed);
307 (void)signal(SIGINT, killed);
308
309 if (enable_threads) {
310 if (init_sem > 0) {
311 TestRealWorld(init_sem);
312 }
313
314 TestWaitTimeout();
315 }
316
317 TestOverheadUncontended();
318
319 if (enable_threads) {
320 TestOverheadContended(false);
321
322 TestOverheadContended(true);
323 }
324
325 SDL_Quit();
326 SDLTest_CommonDestroyState(state);
327
328 return 0;
329}