15ca02815Sjsg // SPDX-License-Identifier: MIT
2c349dbc7Sjsg /*
3c349dbc7Sjsg * Copyright © 2019 Intel Corporation
4c349dbc7Sjsg */
5c349dbc7Sjsg
6ad8b1aafSjsg #include "i915_drv.h"
7c349dbc7Sjsg #include "i915_request.h"
8c349dbc7Sjsg
9c349dbc7Sjsg #include "intel_context.h"
10c349dbc7Sjsg #include "intel_engine_heartbeat.h"
11c349dbc7Sjsg #include "intel_engine_pm.h"
12c349dbc7Sjsg #include "intel_engine.h"
13c349dbc7Sjsg #include "intel_gt.h"
14c349dbc7Sjsg #include "intel_reset.h"
15c349dbc7Sjsg
16c349dbc7Sjsg /*
17c349dbc7Sjsg * While the engine is active, we send a periodic pulse along the engine
18c349dbc7Sjsg * to check on its health and to flush any idle-barriers. If that request
19c349dbc7Sjsg * is stuck, and we fail to preempt it, we declare the engine hung and
20c349dbc7Sjsg * issue a reset -- in the hope that restores progress.
21c349dbc7Sjsg */
22c349dbc7Sjsg
next_heartbeat(struct intel_engine_cs * engine)23c349dbc7Sjsg static bool next_heartbeat(struct intel_engine_cs *engine)
24c349dbc7Sjsg {
25*f005ef32Sjsg struct i915_request *rq;
26c349dbc7Sjsg long delay;
27c349dbc7Sjsg
28c349dbc7Sjsg delay = READ_ONCE(engine->props.heartbeat_interval_ms);
29*f005ef32Sjsg
30*f005ef32Sjsg rq = engine->heartbeat.systole;
31*f005ef32Sjsg
32*f005ef32Sjsg /*
33*f005ef32Sjsg * FIXME: The final period extension is disabled if the period has been
34*f005ef32Sjsg * modified from the default. This is to prevent issues with certain
35*f005ef32Sjsg * selftests which override the value and expect specific behaviour.
36*f005ef32Sjsg * Once the selftests have been updated to either cope with variable
37*f005ef32Sjsg * heartbeat periods (or to override the pre-emption timeout as well,
38*f005ef32Sjsg * or just to add a selftest specific override of the extension), the
39*f005ef32Sjsg * generic override can be removed.
40*f005ef32Sjsg */
41*f005ef32Sjsg if (rq && rq->sched.attr.priority >= I915_PRIORITY_BARRIER &&
42*f005ef32Sjsg delay == engine->defaults.heartbeat_interval_ms) {
43*f005ef32Sjsg long longer;
44*f005ef32Sjsg
45*f005ef32Sjsg /*
46*f005ef32Sjsg * The final try is at the highest priority possible. Up until now
47*f005ef32Sjsg * a pre-emption might not even have been attempted. So make sure
48*f005ef32Sjsg * this last attempt allows enough time for a pre-emption to occur.
49*f005ef32Sjsg */
50*f005ef32Sjsg longer = READ_ONCE(engine->props.preempt_timeout_ms) * 2;
51*f005ef32Sjsg longer = intel_clamp_heartbeat_interval_ms(engine, longer);
52*f005ef32Sjsg if (longer > delay)
53*f005ef32Sjsg delay = longer;
54*f005ef32Sjsg }
55*f005ef32Sjsg
56c349dbc7Sjsg if (!delay)
57c349dbc7Sjsg return false;
58c349dbc7Sjsg
59c349dbc7Sjsg delay = msecs_to_jiffies_timeout(delay);
60c349dbc7Sjsg if (delay >= HZ)
61c349dbc7Sjsg delay = round_jiffies_up_relative(delay);
625ca02815Sjsg mod_delayed_work(system_highpri_wq, &engine->heartbeat.work, delay + 1);
63c349dbc7Sjsg
64c349dbc7Sjsg return true;
65c349dbc7Sjsg }
66c349dbc7Sjsg
675ca02815Sjsg static struct i915_request *
heartbeat_create(struct intel_context * ce,gfp_t gfp)685ca02815Sjsg heartbeat_create(struct intel_context *ce, gfp_t gfp)
695ca02815Sjsg {
705ca02815Sjsg struct i915_request *rq;
715ca02815Sjsg
725ca02815Sjsg intel_context_enter(ce);
735ca02815Sjsg rq = __i915_request_create(ce, gfp);
745ca02815Sjsg intel_context_exit(ce);
755ca02815Sjsg
765ca02815Sjsg return rq;
775ca02815Sjsg }
785ca02815Sjsg
idle_pulse(struct intel_engine_cs * engine,struct i915_request * rq)79c349dbc7Sjsg static void idle_pulse(struct intel_engine_cs *engine, struct i915_request *rq)
80c349dbc7Sjsg {
81c349dbc7Sjsg engine->wakeref_serial = READ_ONCE(engine->serial) + 1;
82c349dbc7Sjsg i915_request_add_active_barriers(rq);
835ca02815Sjsg if (!engine->heartbeat.systole && intel_engine_has_heartbeat(engine))
845ca02815Sjsg engine->heartbeat.systole = i915_request_get(rq);
855ca02815Sjsg }
865ca02815Sjsg
heartbeat_commit(struct i915_request * rq,const struct i915_sched_attr * attr)875ca02815Sjsg static void heartbeat_commit(struct i915_request *rq,
885ca02815Sjsg const struct i915_sched_attr *attr)
895ca02815Sjsg {
905ca02815Sjsg idle_pulse(rq->engine, rq);
915ca02815Sjsg
925ca02815Sjsg __i915_request_commit(rq);
935ca02815Sjsg __i915_request_queue(rq, attr);
94c349dbc7Sjsg }
95c349dbc7Sjsg
show_heartbeat(const struct i915_request * rq,struct intel_engine_cs * engine)96c349dbc7Sjsg static void show_heartbeat(const struct i915_request *rq,
97c349dbc7Sjsg struct intel_engine_cs *engine)
98c349dbc7Sjsg {
99c349dbc7Sjsg struct drm_printer p = drm_debug_printer("heartbeat");
100c349dbc7Sjsg
1015ca02815Sjsg if (!rq) {
1025ca02815Sjsg intel_engine_dump(engine, &p,
1035ca02815Sjsg "%s heartbeat not ticking\n",
1045ca02815Sjsg engine->name);
1055ca02815Sjsg } else {
106c349dbc7Sjsg intel_engine_dump(engine, &p,
107ad8b1aafSjsg "%s heartbeat {seqno:%llx:%lld, prio:%d} not ticking\n",
108c349dbc7Sjsg engine->name,
109ad8b1aafSjsg rq->fence.context,
110ad8b1aafSjsg rq->fence.seqno,
111c349dbc7Sjsg rq->sched.attr.priority);
112c349dbc7Sjsg }
1135ca02815Sjsg }
1145ca02815Sjsg
1155ca02815Sjsg static void
reset_engine(struct intel_engine_cs * engine,struct i915_request * rq)1165ca02815Sjsg reset_engine(struct intel_engine_cs *engine, struct i915_request *rq)
1175ca02815Sjsg {
1185ca02815Sjsg if (IS_ENABLED(CONFIG_DRM_I915_DEBUG_GEM))
1195ca02815Sjsg show_heartbeat(rq, engine);
1205ca02815Sjsg
1215ca02815Sjsg if (intel_engine_uses_guc(engine))
1225ca02815Sjsg /*
1235ca02815Sjsg * GuC itself is toast or GuC's hang detection
1245ca02815Sjsg * is disabled. Either way, need to find the
1255ca02815Sjsg * hang culprit manually.
1265ca02815Sjsg */
1275ca02815Sjsg intel_guc_find_hung_context(engine);
1285ca02815Sjsg
1295ca02815Sjsg intel_gt_handle_error(engine->gt, engine->mask,
1305ca02815Sjsg I915_ERROR_CAPTURE,
1315ca02815Sjsg "stopped heartbeat on %s",
1325ca02815Sjsg engine->name);
1335ca02815Sjsg }
134c349dbc7Sjsg
heartbeat(struct work_struct * wrk)135c349dbc7Sjsg static void heartbeat(struct work_struct *wrk)
136c349dbc7Sjsg {
1375ca02815Sjsg struct i915_sched_attr attr = { .priority = I915_PRIORITY_MIN };
138c349dbc7Sjsg struct intel_engine_cs *engine =
139c349dbc7Sjsg container_of(wrk, typeof(*engine), heartbeat.work.work);
140c349dbc7Sjsg struct intel_context *ce = engine->kernel_context;
141c349dbc7Sjsg struct i915_request *rq;
142ad8b1aafSjsg unsigned long serial;
143ad8b1aafSjsg
144ad8b1aafSjsg /* Just in case everything has gone horribly wrong, give it a kick */
145ad8b1aafSjsg intel_engine_flush_submission(engine);
146c349dbc7Sjsg
147c349dbc7Sjsg rq = engine->heartbeat.systole;
148c349dbc7Sjsg if (rq && i915_request_completed(rq)) {
149c349dbc7Sjsg i915_request_put(rq);
150c349dbc7Sjsg engine->heartbeat.systole = NULL;
151c349dbc7Sjsg }
152c349dbc7Sjsg
153c349dbc7Sjsg if (!intel_engine_pm_get_if_awake(engine))
154c349dbc7Sjsg return;
155c349dbc7Sjsg
156c349dbc7Sjsg if (intel_gt_is_wedged(engine->gt))
157c349dbc7Sjsg goto out;
158c349dbc7Sjsg
1595ca02815Sjsg if (i915_sched_engine_disabled(engine->sched_engine)) {
1605ca02815Sjsg reset_engine(engine, engine->heartbeat.systole);
1615ca02815Sjsg goto out;
1625ca02815Sjsg }
1635ca02815Sjsg
164c349dbc7Sjsg if (engine->heartbeat.systole) {
1655ca02815Sjsg long delay = READ_ONCE(engine->props.heartbeat_interval_ms);
1665ca02815Sjsg
1675ca02815Sjsg /* Safeguard against too-fast worker invocations */
1685ca02815Sjsg if (!time_after(jiffies,
1695ca02815Sjsg rq->emitted_jiffies + msecs_to_jiffies(delay)))
1705ca02815Sjsg goto out;
1715ca02815Sjsg
172ad8b1aafSjsg if (!i915_sw_fence_signaled(&rq->submit)) {
173ad8b1aafSjsg /*
174ad8b1aafSjsg * Not yet submitted, system is stalled.
175ad8b1aafSjsg *
176ad8b1aafSjsg * This more often happens for ring submission,
177ad8b1aafSjsg * where all contexts are funnelled into a common
178ad8b1aafSjsg * ringbuffer. If one context is blocked on an
179ad8b1aafSjsg * external fence, not only is it not submitted,
180ad8b1aafSjsg * but all other contexts, including the kernel
181ad8b1aafSjsg * context are stuck waiting for the signal.
182ad8b1aafSjsg */
1835ca02815Sjsg } else if (engine->sched_engine->schedule &&
184c349dbc7Sjsg rq->sched.attr.priority < I915_PRIORITY_BARRIER) {
185c349dbc7Sjsg /*
186c349dbc7Sjsg * Gradually raise the priority of the heartbeat to
187c349dbc7Sjsg * give high priority work [which presumably desires
188c349dbc7Sjsg * low latency and no jitter] the chance to naturally
189c349dbc7Sjsg * complete before being preempted.
190c349dbc7Sjsg */
1915ca02815Sjsg attr.priority = 0;
192c349dbc7Sjsg if (rq->sched.attr.priority >= attr.priority)
1935ca02815Sjsg attr.priority = I915_PRIORITY_HEARTBEAT;
194c349dbc7Sjsg if (rq->sched.attr.priority >= attr.priority)
195c349dbc7Sjsg attr.priority = I915_PRIORITY_BARRIER;
196c349dbc7Sjsg
197c349dbc7Sjsg local_bh_disable();
1985ca02815Sjsg engine->sched_engine->schedule(rq, &attr);
199c349dbc7Sjsg local_bh_enable();
200c349dbc7Sjsg } else {
2015ca02815Sjsg reset_engine(engine, rq);
202c349dbc7Sjsg }
2035ca02815Sjsg
2045ca02815Sjsg rq->emitted_jiffies = jiffies;
205c349dbc7Sjsg goto out;
206c349dbc7Sjsg }
207c349dbc7Sjsg
208ad8b1aafSjsg serial = READ_ONCE(engine->serial);
209ad8b1aafSjsg if (engine->wakeref_serial == serial)
210c349dbc7Sjsg goto out;
211c349dbc7Sjsg
212ad8b1aafSjsg if (!mutex_trylock(&ce->timeline->mutex)) {
213ad8b1aafSjsg /* Unable to lock the kernel timeline, is the engine stuck? */
214ad8b1aafSjsg if (xchg(&engine->heartbeat.blocked, serial) == serial)
215ad8b1aafSjsg intel_gt_handle_error(engine->gt, engine->mask,
216ad8b1aafSjsg I915_ERROR_CAPTURE,
217ad8b1aafSjsg "no heartbeat on %s",
218ad8b1aafSjsg engine->name);
219ad8b1aafSjsg goto out;
220ad8b1aafSjsg }
221c349dbc7Sjsg
2225ca02815Sjsg rq = heartbeat_create(ce, GFP_NOWAIT | __GFP_NOWARN);
223c349dbc7Sjsg if (IS_ERR(rq))
224c349dbc7Sjsg goto unlock;
225c349dbc7Sjsg
2265ca02815Sjsg heartbeat_commit(rq, &attr);
227c349dbc7Sjsg
228c349dbc7Sjsg unlock:
229c349dbc7Sjsg mutex_unlock(&ce->timeline->mutex);
230c349dbc7Sjsg out:
2315ca02815Sjsg if (!engine->i915->params.enable_hangcheck || !next_heartbeat(engine))
232c349dbc7Sjsg i915_request_put(fetch_and_zero(&engine->heartbeat.systole));
233c349dbc7Sjsg intel_engine_pm_put(engine);
234c349dbc7Sjsg }
235c349dbc7Sjsg
intel_engine_unpark_heartbeat(struct intel_engine_cs * engine)236c349dbc7Sjsg void intel_engine_unpark_heartbeat(struct intel_engine_cs *engine)
237c349dbc7Sjsg {
2381bb76ff1Sjsg if (!CONFIG_DRM_I915_HEARTBEAT_INTERVAL)
239c349dbc7Sjsg return;
240c349dbc7Sjsg
241c349dbc7Sjsg next_heartbeat(engine);
242c349dbc7Sjsg }
243c349dbc7Sjsg
intel_engine_park_heartbeat(struct intel_engine_cs * engine)244c349dbc7Sjsg void intel_engine_park_heartbeat(struct intel_engine_cs *engine)
245c349dbc7Sjsg {
246c349dbc7Sjsg if (cancel_delayed_work(&engine->heartbeat.work))
247c349dbc7Sjsg i915_request_put(fetch_and_zero(&engine->heartbeat.systole));
248c349dbc7Sjsg }
249c349dbc7Sjsg
intel_gt_unpark_heartbeats(struct intel_gt * gt)2505ca02815Sjsg void intel_gt_unpark_heartbeats(struct intel_gt *gt)
2515ca02815Sjsg {
2525ca02815Sjsg struct intel_engine_cs *engine;
2535ca02815Sjsg enum intel_engine_id id;
2545ca02815Sjsg
2555ca02815Sjsg for_each_engine(engine, gt, id)
2565ca02815Sjsg if (intel_engine_pm_is_awake(engine))
2575ca02815Sjsg intel_engine_unpark_heartbeat(engine);
2585ca02815Sjsg }
2595ca02815Sjsg
intel_gt_park_heartbeats(struct intel_gt * gt)2605ca02815Sjsg void intel_gt_park_heartbeats(struct intel_gt *gt)
2615ca02815Sjsg {
2625ca02815Sjsg struct intel_engine_cs *engine;
2635ca02815Sjsg enum intel_engine_id id;
2645ca02815Sjsg
2655ca02815Sjsg for_each_engine(engine, gt, id)
2665ca02815Sjsg intel_engine_park_heartbeat(engine);
2675ca02815Sjsg }
2685ca02815Sjsg
intel_engine_init_heartbeat(struct intel_engine_cs * engine)269c349dbc7Sjsg void intel_engine_init_heartbeat(struct intel_engine_cs *engine)
270c349dbc7Sjsg {
271c349dbc7Sjsg INIT_DELAYED_WORK(&engine->heartbeat.work, heartbeat);
272c349dbc7Sjsg }
273c349dbc7Sjsg
__intel_engine_pulse(struct intel_engine_cs * engine)274ad8b1aafSjsg static int __intel_engine_pulse(struct intel_engine_cs *engine)
275c349dbc7Sjsg {
276c349dbc7Sjsg struct i915_sched_attr attr = { .priority = I915_PRIORITY_BARRIER };
277c349dbc7Sjsg struct intel_context *ce = engine->kernel_context;
278c349dbc7Sjsg struct i915_request *rq;
279ad8b1aafSjsg
280ad8b1aafSjsg lockdep_assert_held(&ce->timeline->mutex);
281ad8b1aafSjsg GEM_BUG_ON(!intel_engine_has_preemption(engine));
282ad8b1aafSjsg GEM_BUG_ON(!intel_engine_pm_is_awake(engine));
283ad8b1aafSjsg
2845ca02815Sjsg rq = heartbeat_create(ce, GFP_NOWAIT | __GFP_NOWARN);
285ad8b1aafSjsg if (IS_ERR(rq))
286ad8b1aafSjsg return PTR_ERR(rq);
287ad8b1aafSjsg
288ad8b1aafSjsg __set_bit(I915_FENCE_FLAG_SENTINEL, &rq->fence.flags);
289ad8b1aafSjsg
2905ca02815Sjsg heartbeat_commit(rq, &attr);
291ad8b1aafSjsg GEM_BUG_ON(rq->sched.attr.priority < I915_PRIORITY_BARRIER);
292ad8b1aafSjsg
293ad8b1aafSjsg return 0;
294ad8b1aafSjsg }
295ad8b1aafSjsg
set_heartbeat(struct intel_engine_cs * engine,unsigned long delay)296ad8b1aafSjsg static unsigned long set_heartbeat(struct intel_engine_cs *engine,
297ad8b1aafSjsg unsigned long delay)
298ad8b1aafSjsg {
299ad8b1aafSjsg unsigned long old;
300ad8b1aafSjsg
301ad8b1aafSjsg old = xchg(&engine->props.heartbeat_interval_ms, delay);
302ad8b1aafSjsg if (delay)
303ad8b1aafSjsg intel_engine_unpark_heartbeat(engine);
304ad8b1aafSjsg else
305ad8b1aafSjsg intel_engine_park_heartbeat(engine);
306ad8b1aafSjsg
307ad8b1aafSjsg return old;
308ad8b1aafSjsg }
309ad8b1aafSjsg
intel_engine_set_heartbeat(struct intel_engine_cs * engine,unsigned long delay)310ad8b1aafSjsg int intel_engine_set_heartbeat(struct intel_engine_cs *engine,
311ad8b1aafSjsg unsigned long delay)
312ad8b1aafSjsg {
313ad8b1aafSjsg struct intel_context *ce = engine->kernel_context;
314ad8b1aafSjsg int err = 0;
315ad8b1aafSjsg
316ad8b1aafSjsg if (!delay && !intel_engine_has_preempt_reset(engine))
317ad8b1aafSjsg return -ENODEV;
318ad8b1aafSjsg
319*f005ef32Sjsg /* FIXME: Remove together with equally marked hack in next_heartbeat. */
320*f005ef32Sjsg if (delay != engine->defaults.heartbeat_interval_ms &&
321*f005ef32Sjsg delay < 2 * engine->props.preempt_timeout_ms) {
322*f005ef32Sjsg if (intel_engine_uses_guc(engine))
323*f005ef32Sjsg drm_notice(&engine->i915->drm, "%s heartbeat interval adjusted to a non-default value which may downgrade individual engine resets to full GPU resets!\n",
324*f005ef32Sjsg engine->name);
325*f005ef32Sjsg else
326*f005ef32Sjsg drm_notice(&engine->i915->drm, "%s heartbeat interval adjusted to a non-default value which may cause engine resets to target innocent contexts!\n",
327*f005ef32Sjsg engine->name);
328*f005ef32Sjsg }
329*f005ef32Sjsg
330ad8b1aafSjsg intel_engine_pm_get(engine);
331ad8b1aafSjsg
332ad8b1aafSjsg err = mutex_lock_interruptible(&ce->timeline->mutex);
333ad8b1aafSjsg if (err)
334ad8b1aafSjsg goto out_rpm;
335ad8b1aafSjsg
336ad8b1aafSjsg if (delay != engine->props.heartbeat_interval_ms) {
337ad8b1aafSjsg unsigned long saved = set_heartbeat(engine, delay);
338ad8b1aafSjsg
339ad8b1aafSjsg /* recheck current execution */
340ad8b1aafSjsg if (intel_engine_has_preemption(engine)) {
341ad8b1aafSjsg err = __intel_engine_pulse(engine);
342ad8b1aafSjsg if (err)
343ad8b1aafSjsg set_heartbeat(engine, saved);
344ad8b1aafSjsg }
345ad8b1aafSjsg }
346ad8b1aafSjsg
347ad8b1aafSjsg mutex_unlock(&ce->timeline->mutex);
348ad8b1aafSjsg
349ad8b1aafSjsg out_rpm:
350ad8b1aafSjsg intel_engine_pm_put(engine);
351ad8b1aafSjsg return err;
352ad8b1aafSjsg }
353ad8b1aafSjsg
intel_engine_pulse(struct intel_engine_cs * engine)354ad8b1aafSjsg int intel_engine_pulse(struct intel_engine_cs *engine)
355ad8b1aafSjsg {
356ad8b1aafSjsg struct intel_context *ce = engine->kernel_context;
357c349dbc7Sjsg int err;
358c349dbc7Sjsg
359c349dbc7Sjsg if (!intel_engine_has_preemption(engine))
360c349dbc7Sjsg return -ENODEV;
361c349dbc7Sjsg
362c349dbc7Sjsg if (!intel_engine_pm_get_if_awake(engine))
363c349dbc7Sjsg return 0;
364c349dbc7Sjsg
365c349dbc7Sjsg err = -EINTR;
366ad8b1aafSjsg if (!mutex_lock_interruptible(&ce->timeline->mutex)) {
367ad8b1aafSjsg err = __intel_engine_pulse(engine);
368c349dbc7Sjsg mutex_unlock(&ce->timeline->mutex);
369ad8b1aafSjsg }
370ad8b1aafSjsg
3715ca02815Sjsg intel_engine_flush_submission(engine);
372c349dbc7Sjsg intel_engine_pm_put(engine);
373c349dbc7Sjsg return err;
374c349dbc7Sjsg }
375c349dbc7Sjsg
intel_engine_flush_barriers(struct intel_engine_cs * engine)376c349dbc7Sjsg int intel_engine_flush_barriers(struct intel_engine_cs *engine)
377c349dbc7Sjsg {
3785ca02815Sjsg struct i915_sched_attr attr = { .priority = I915_PRIORITY_MIN };
3795ca02815Sjsg struct intel_context *ce = engine->kernel_context;
380c349dbc7Sjsg struct i915_request *rq;
3815ca02815Sjsg int err;
382c349dbc7Sjsg
383c349dbc7Sjsg if (llist_empty(&engine->barrier_tasks))
384c349dbc7Sjsg return 0;
385c349dbc7Sjsg
386c349dbc7Sjsg if (!intel_engine_pm_get_if_awake(engine))
387c349dbc7Sjsg return 0;
388c349dbc7Sjsg
3895ca02815Sjsg if (mutex_lock_interruptible(&ce->timeline->mutex)) {
3905ca02815Sjsg err = -EINTR;
391c349dbc7Sjsg goto out_rpm;
392c349dbc7Sjsg }
393c349dbc7Sjsg
3945ca02815Sjsg rq = heartbeat_create(ce, GFP_KERNEL);
3955ca02815Sjsg if (IS_ERR(rq)) {
3965ca02815Sjsg err = PTR_ERR(rq);
3975ca02815Sjsg goto out_unlock;
3985ca02815Sjsg }
399c349dbc7Sjsg
4005ca02815Sjsg heartbeat_commit(rq, &attr);
4015ca02815Sjsg
4025ca02815Sjsg err = 0;
4035ca02815Sjsg out_unlock:
4045ca02815Sjsg mutex_unlock(&ce->timeline->mutex);
405c349dbc7Sjsg out_rpm:
406c349dbc7Sjsg intel_engine_pm_put(engine);
407c349dbc7Sjsg return err;
408c349dbc7Sjsg }
409c349dbc7Sjsg
410c349dbc7Sjsg #if IS_ENABLED(CONFIG_DRM_I915_SELFTEST)
411c349dbc7Sjsg #include "selftest_engine_heartbeat.c"
412c349dbc7Sjsg #endif
413