| File: | /shared/playproj/i2c/src/dw/dw_suspend_resume.c |
| Warning: | line 254, column 11 Value stored to 'result' during its initialization is never read |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /** @file dw_suspend_resume.c |
| 2 | * Detection of system resume from sleep, combining every available method |
| 3 | * |
| 4 | * Consults the logind PrepareForSleep signals recorded by dbus_util.c together |
| 5 | * with the clock comparison in util/suspend_resume_util.c. The clock half is |
| 6 | * separate because it needs no dbus and has a util-layer consumer; this half |
| 7 | * does need dbus and belongs with the display watch code that drives it. |
| 8 | */ |
| 9 | |
| 10 | // Copyright (C) 2026 Sanford Rockowitz <rockowitz@minsoft.com> |
| 11 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 12 | |
| 13 | #include "config.h" |
| 14 | #include <inttypes.h> |
| 15 | #include <stdbool.h> |
| 16 | #include <stdint.h> |
| 17 | #include <time.h> |
| 18 | #include <syslog.h> |
| 19 | #include "util/debug_util.h" |
| 20 | #include "util/dbus_util.h" |
| 21 | #include "util/report_util.h" |
| 22 | #include "util/string_util.h" |
| 23 | #include "util/syslog_util.h" |
| 24 | #include "util/timestamp.h" |
| 25 | #include "util/suspend_resume_util.h" |
| 26 | |
| 27 | #include "dw_suspend_resume.h" |
| 28 | |
| 29 | // |
| 30 | // Combined CLOCKTIME/BOOTTIME and dbus algorithm |
| 31 | // |
| 32 | |
| 33 | /** Determines whether the system recently resumed from sleep, consulting |
| 34 | * every available detection method. |
| 35 | * |
| 36 | * @param within_ms interval that defines "recently" |
| 37 | * @param millisec_since_loc if non-NULL, where to return the number of |
| 38 | * milliseconds since the resume, UINT64_MAX if |
| 39 | * no recent resume |
| 40 | * @param detection_loc if non-NULL, where to return which method |
| 41 | * answered. A caller that reports the pause it |
| 42 | * takes needs this: only two of the three |
| 43 | * methods establish that the system has actually |
| 44 | * slept. See resume_detection_description(). |
| 45 | * @return true if a resume from sleep occurred within the past **within_ms** |
| 46 | * |
| 47 | * @remark |
| 48 | * Callers pause for the time remaining in their own interval, rather than |
| 49 | * this function pausing, because each has its own sleep and logging needs. |
| 50 | * |
| 51 | * ddcutil detects a resume from sleep three ways. They are complementary, |
| 52 | * not redundant, and none alone is sufficient. |
| 53 | * |
| 54 | * The **dbus** method (dbus_util.c) records when the logind |
| 55 | * **PrepareForSleep(false)** signal is received. It is precise, it is |
| 56 | * process wide, and it covers three cases the clock method cannot: |
| 57 | * - **Program start.** ldbus_elapsed_since_resume_from_sleep_mark_start() |
| 58 | * deliberately treats the start of the sleep watch thread like a resume, |
| 59 | * because the window just after boot or login has the same transient |
| 60 | * EACCES race on /dev/i2c opens that the window after resume does. The |
| 61 | * clock method reports nothing at startup: no sleep has accumulated. |
| 62 | * - **Short suspends.** The clock method needs more than 1 second of |
| 63 | * accumulated sleep before it reports a resume at all. |
| 64 | * - **Shared timing.** last_resume_from_sleep_ns is a single process wide |
| 65 | * value, so every thread computes the same elapsed time and pauses only |
| 66 | * the remainder of the interval. The clock method's state is per thread, |
| 67 | * so threads detect independently and each starts its own pause from its |
| 68 | * own first look, which can multiply the pauses taken. |
| 69 | * Its weakness is latency. The signal is delivered asynchronously, and the |
| 70 | * display watch thread can be woken by udev and begin reopening buses before |
| 71 | * the dbus thread has processed it. That interval is exactly when udev has |
| 72 | * not yet reapplied the /dev/i2c ACLs, so relying on dbus alone mistakes a |
| 73 | * transient permission failure for a permanent one. |
| 74 | * |
| 75 | * The **clock** method (recently_resumed_from_sleep_by_clocktime()) compares |
| 76 | * CLOCK_BOOTTIME against CLOCK_MONOTONIC. It cannot be late: the divergence |
| 77 | * is already present the instant the thread runs again, and no signal |
| 78 | * delivery is involved. Its weaknesses mirror the strengths above. It is |
| 79 | * coarse, per thread, silent at startup, and its reference point is when a |
| 80 | * thread happened to look, not when the resume occurred. |
| 81 | * |
| 82 | * dbus is therefore preferred where it is trustworthy, and the clock method |
| 83 | * covers the case it cannot: the signal not yet delivered. |
| 84 | * |
| 85 | * The **open sleep cycle** method uses the other logind signal, |
| 86 | * **PrepareForSleep(true)**, whose timestamp dbus_util.c records alongside |
| 87 | * the resume timestamp. A cycle opened by that signal and not yet closed by |
| 88 | * its counterpart means this process is somewhere inside a suspend, and any |
| 89 | * thread running there is treated as having just resumed. It covers what |
| 90 | * neither of the others can: a suspend that stopped this process without |
| 91 | * accumulating sleep the clock method can see. Its weakness is that it |
| 92 | * depends on the closing signal to know the cycle is over; the sleep watch |
| 93 | * thread's own running time bounds what happens when that signal never |
| 94 | * arrives. See the body and ldbus_in_open_sleep_cycle(). |
| 95 | * |
| 96 | * Neither elapsed time can simply be trusted over the other: |
| 97 | * - Taking whichever is smaller prefers the clock method systematically, |
| 98 | * since its reference point is the later one whenever detection lags the |
| 99 | * resume, and pauses for the full interval well after the resume: resume |
| 100 | * at T, dbus signal at T+50 ms, first call on this thread at T+3000 ms, |
| 101 | * elapsed reported as 0 rather than 3000. |
| 102 | * - Taking dbus whenever it is within the interval lets a timestamp that |
| 103 | * predates the suspend, from an earlier resume or from |
| 104 | * ldbus_elapsed_since_resume_from_sleep_mark_start(), mask a detection |
| 105 | * that is genuinely fresh, and pauses too little. |
| 106 | * |
| 107 | * The tie is broken on whether the clock method detected the resume on this |
| 108 | * very call, which it reports through its out parameter. A detection now |
| 109 | * means this thread had not yet accounted for the sleep, so dbus may not |
| 110 | * have processed the signal either and its timestamp may predate the |
| 111 | * suspend; the clock is preferred. Inside the grace window the resume has |
| 112 | * already been observed once, dbus has had time to catch up, and it is the |
| 113 | * more accurate of the two. See the body for why the two errors are not |
| 114 | * symmetric. Note that the decision deliberately does not depend on |
| 115 | * within_ms, which is tunable and must not determine correctness. |
| 116 | */ |
| 117 | bool_Bool recently_resumed_from_sleep(int within_ms, uint64_t * millisec_since_loc, |
| 118 | Resume_Detection * detection_loc) |
| 119 | { |
| 120 | bool_Bool debug = false0; |
| 121 | bool_Bool resumed = false0; |
| 122 | uint64_t millisec_since = UINT64_MAX(18446744073709551615UL); |
| 123 | Resume_Detection detection = RESUME_DETECTED_NONE; |
| 124 | |
| 125 | // Called on every invocation, whatever dbus reports, so that this thread's |
| 126 | // baseline stays current and its grace window opens when the resume is |
| 127 | // first observed here. Otherwise the first call on a thread where dbus |
| 128 | // always won the race would report a resume that was long since handled. |
| 129 | bool_Bool clock_detected_now = false0; |
| 130 | bool_Bool resumed_by_clocktime = recently_resumed_from_sleep_by_clocktime(&clock_detected_now); |
| 131 | |
| 132 | #ifdef USE_DBUS1 |
| 133 | // An open sleep cycle, i.e. a PrepareForSleep(true) not yet matched by a |
| 134 | // PrepareForSleep(false), is reported as a resume whatever the clocks say. |
| 135 | // |
| 136 | // Once the kernel has frozen user space this process cannot run again until |
| 137 | // it is thawed, so a thread executing inside an open cycle has either been |
| 138 | // thawed already or is in the interval between the signal and the freeze. |
| 139 | // Neither other source covers the first case: |
| 140 | // - dbus has not yet dispatched PrepareForSleep(false). That is the |
| 141 | // latency described above, and the resume timestamp it would report |
| 142 | // still predates the suspend. |
| 143 | // - the clock method needs more than a second of accumulated sleep, which |
| 144 | // the cycle need never have produced. Freezing user space precedes |
| 145 | // timekeeping_suspend() and thawing follows timekeeping_resume(), so a |
| 146 | // suspend that is aborted, or whose device callbacks are slow (hybrid |
| 147 | // graphics, notably), can stop this process for ten seconds while |
| 148 | // BOOTTIME and MONOTONIC stay in lockstep and nothing is detected. The |
| 149 | // /dev/i2c ACLs are dropped and reapplied around the whole cycle, not |
| 150 | // around its sleeping part, so the EACCES window is there regardless. |
| 151 | // |
| 152 | // Reported as elapsed 0, so the caller pauses its full interval. While the |
| 153 | // cycle is open there is no better reference point: the prepare timestamp |
| 154 | // marks the start of the cycle, not the resume, and measuring from it would |
| 155 | // count the entire suspend as already elapsed and pause not at all. |
| 156 | // |
| 157 | // Before the freeze this reports a resume that has not occurred. That |
| 158 | // costs one interval per call over a window logind bounds by |
| 159 | // InhibitDelayMaxSec, 5 seconds by default, and ddcutil holds no delay |
| 160 | // inhibitor, so the freeze normally follows the signal promptly. It also |
| 161 | // keeps the watch thread from opening buses while the GPU is being torn |
| 162 | // down, which is no worse a place to be idle. |
| 163 | // |
| 164 | // The rule holds until the matching signal arrives, or, should it never |
| 165 | // arrive, until the sleep watch thread has run long enough since the |
| 166 | // prepare signal to conclude it is not coming. See |
| 167 | // ldbus_in_open_sleep_cycle(). |
| 168 | bool_Bool sleep_cycle_open = ldbus_in_open_sleep_cycle(NULL((void*)0)); |
| 169 | |
| 170 | // within_ms 0 asks whether a resume occurred within no time at all, and the |
| 171 | // answer must remain no: callers subtract millisec_since from within_ms. |
| 172 | if (sleep_cycle_open && within_ms > 0) { |
| 173 | resumed = true1; |
| 174 | millisec_since = 0; |
| 175 | detection = RESUME_DETECTED_IN_SLEEP_CYCLE; |
| 176 | } |
| 177 | |
| 178 | uint64_t dbus_elapsed_ms = NANOS2MILLIS(ldbus_elapsed_since_resume_from_sleep_ns()); |
| 179 | |
| 180 | // A detection on THIS call means sleep accumulated that this thread had |
| 181 | // not yet accounted for, so dbus may not have processed the corresponding |
| 182 | // signal either; its timestamp can predate the suspend, and measuring |
| 183 | // from it would pause far too little. The clock is preferred in that |
| 184 | // case. Within the grace window the resume has been observed at least |
| 185 | // once already, dbus has had time to catch up, and its timestamp is the |
| 186 | // more accurate of the two, so it is preferred there. |
| 187 | // |
| 188 | // The two errors are not symmetric, which is what settles the direction. |
| 189 | // Preferring the clock when dbus was in fact current costs one interval |
| 190 | // of extra pause, latency and nothing more. Preferring dbus when its |
| 191 | // timestamp is stale reopens the buses while udev has not yet reapplied |
| 192 | // the ACLs, which costs the whole EACCES retry ladder, up to |
| 193 | // max_eacces_retry_ms, plus a diagnostic dump in the system log. |
| 194 | // |
| 195 | // Deliberately not decided by comparing the two elapsed times, nor by |
| 196 | // comparing either against within_ms: within_ms is tunable and must not |
| 197 | // be what determines correctness. |
| 198 | // |
| 199 | // Not consulted when the cycle is open: its timestamp is then known to |
| 200 | // predate the suspend, and 0 is already the conservative answer. |
| 201 | if (!resumed && !clock_detected_now && dbus_elapsed_ms < (uint64_t) within_ms) { |
| 202 | resumed = true1; |
| 203 | millisec_since = dbus_elapsed_ms; |
| 204 | detection = RESUME_DETECTED_BY_DBUS; |
| 205 | } |
| 206 | #endif |
| 207 | |
| 208 | // Fallback, for the case dbus cannot cover: the signal has not yet been |
| 209 | // delivered, or the build has no dbus support. |
| 210 | // |
| 211 | // This branch is the one that reports a resume when the clock method is |
| 212 | // wrong, and it is reached whatever dbus says, so a false detection here |
| 213 | // is not masked by a correct dbus answer. It surfaced as |
| 214 | // test_dw_suspend_resume failing on the aarch64 worker at |
| 215 | // build.opensuse.org while passing on x86_64: the two assertions in that |
| 216 | // test expecting false are the only ones this branch can reach. The |
| 217 | // cause was in get_accumulated_sleep_ns(), not here or in the test -- |
| 218 | // nothing about it is specific to aarch64, only to a machine whose |
| 219 | // BOOTTIME/MONOTONIC offset sits near zero, which is every worker that |
| 220 | // has never suspended. |
| 221 | if (!resumed && resumed_by_clocktime) { |
| 222 | uint64_t clock_elapsed_ms = millisec_since_resume_detected_by_clocktime(); |
| 223 | if (clock_elapsed_ms < (uint64_t) within_ms) { |
| 224 | resumed = true1; |
| 225 | millisec_since = clock_elapsed_ms; |
| 226 | detection = RESUME_DETECTED_BY_CLOCKTIME; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | if (millisec_since_loc) |
| 231 | *millisec_since_loc = millisec_since; |
| 232 | if (detection_loc) |
| 233 | *detection_loc = detection; |
| 234 | DBGF(debug, "within_ms=%d, millisec_since=%"PRIu64", detection=%s, returning %s",do { if (debug) simple_dbgmsg(debug, __func__, 236, "dw_suspend_resume.c" , "within_ms=%d, millisec_since=%""l" "u"", detection=%s, returning %s" , within_ms, millisec_since, resume_detection_description(detection ), sbool(resumed)); } while(0) |
| 235 | within_ms, millisec_since, resume_detection_description(detection),do { if (debug) simple_dbgmsg(debug, __func__, 236, "dw_suspend_resume.c" , "within_ms=%d, millisec_since=%""l" "u"", detection=%s, returning %s" , within_ms, millisec_since, resume_detection_description(detection ), sbool(resumed)); } while(0) |
| 236 | sbool(resumed))do { if (debug) simple_dbgmsg(debug, __func__, 236, "dw_suspend_resume.c" , "within_ms=%d, millisec_since=%""l" "u"", detection=%s, returning %s" , within_ms, millisec_since, resume_detection_description(detection ), sbool(resumed)); } while(0); |
| 237 | return resumed; |
| 238 | } |
| 239 | |
| 240 | |
| 241 | /** Returns a description of how a resume from sleep was detected, phrased as |
| 242 | * the opening clause of a message reporting it. |
| 243 | * |
| 244 | * An open sleep cycle is deliberately not described as a resume. The pause |
| 245 | * it causes may be taken before the system has slept at all, in the interval |
| 246 | * between PrepareForSleep(true) and the freeze, and a message claiming a |
| 247 | * resume there would contradict the machine's state in the system log at |
| 248 | * exactly the point where this subsystem is diagnosed from it. |
| 249 | * |
| 250 | * @param detection value reported by #recently_resumed_from_sleep() |
| 251 | * @return description, valid for the life of the program |
| 252 | */ |
| 253 | const char * resume_detection_description(Resume_Detection detection) { |
| 254 | char * result = "Unrecognized resume detection"; |
Value stored to 'result' during its initialization is never read | |
| 255 | switch(detection) { |
| 256 | case RESUME_DETECTED_NONE: |
| 257 | result = "No recent resume from sleep"; break; |
| 258 | case RESUME_DETECTED_BY_DBUS: |
| 259 | result = "Recently resumed from sleep, per dbus signal"; break; |
| 260 | case RESUME_DETECTED_BY_CLOCKTIME: |
| 261 | result = "Recently resumed from sleep, per clock comparison"; break; |
| 262 | case RESUME_DETECTED_IN_SLEEP_CYCLE: |
| 263 | result = "Sleep cycle in progress"; break; |
| 264 | } |
| 265 | return result; |
| 266 | } |