Feat/Health check (#4047)

* Message on low log loss

* make console.log permanent

* Added: Health check option

* disable button

* change health check conditions

* i18n

* ./check

* Apply suggestions from code review

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* delete shadowed fsrs

* Update ts/routes/deck-options/FsrsOptions.svelte

Co-authored-by: llama <gh@siid.sh>

* Update ftl/core/deck-config.ftl

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* anon's suggestions

* snake_case

* capital slow

* make global

* on by default

* Adjusted loss values

* Show message on pass

* ./check

* ComputeParamsRequest

* update coefficients

* update thresholds

* fix thresholds

* Apply suggestions from code review

---------

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
Co-authored-by: llama <gh@siid.sh>
Co-authored-by: Damien Elmes <dae@users.noreply.github.com>
This commit is contained in:
Luc Mcgrady 2025-06-06 06:43:33 +01:00 committed by GitHub
parent 8add993fca
commit 55ecbc1125
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 158 additions and 49 deletions

View file

@ -395,6 +395,7 @@ deck-config-weights = FSRS parameters
deck-config-compute-optimal-weights = Optimize FSRS parameters deck-config-compute-optimal-weights = Optimize FSRS parameters
deck-config-compute-minimum-recommended-retention = Minimum recommended retention deck-config-compute-minimum-recommended-retention = Minimum recommended retention
deck-config-optimize-button = Optimize Current Preset deck-config-optimize-button = Optimize Current Preset
deck-config-health-check = Check health when optimizing (slow)
deck-config-compute-button = Compute deck-config-compute-button = Compute
deck-config-ignore-before = Ignore cards reviewed before deck-config-ignore-before = Ignore cards reviewed before
deck-config-time-to-optimize = It's been a while - using the Optimize All Presets button is recommended. deck-config-time-to-optimize = It's been a while - using the Optimize All Presets button is recommended.
@ -485,6 +486,15 @@ deck-config-percent-input = { $pct }%
deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }... deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
deck-config-fsrs-must-be-enabled = FSRS must be enabled first. deck-config-fsrs-must-be-enabled = FSRS must be enabled first.
deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal. deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal.
deck-config-fsrs-bad-fit-warning = Your memory is difficult for FSRS to predict. Recommendations:
- Suspend or reformulate leeches.
- Use the answer buttons consistently. Keep in mind that "Hard" is a passing grade, not a failing grade.
- Understand before you memorize.
If you follow these suggestions, performance will usually improve over the next few months.
deck-config-fsrs-good-fit = FSRS is well adjusted to your memory.
deck-config-fsrs-params-no-reviews = No reviews found. Make sure this preset is assigned to all decks (including subdecks) that you want to optimize, and try again. deck-config-fsrs-params-no-reviews = No reviews found. Make sure this preset is assigned to all decks (including subdecks) that you want to optimize, and try again.
deck-config-wait-for-audio = Wait for audio deck-config-wait-for-audio = Wait for audio

View file

@ -235,6 +235,7 @@ message DeckConfigsForUpdate {
// only applies to v3 scheduler // only applies to v3 scheduler
bool new_cards_ignore_review_limit = 7; bool new_cards_ignore_review_limit = 7;
bool fsrs = 8; bool fsrs = 8;
bool fsrs_health_check = 11;
bool apply_all_parent_limits = 9; bool apply_all_parent_limits = 9;
uint32 days_since_last_fsrs_optimize = 10; uint32 days_since_last_fsrs_optimize = 10;
} }
@ -258,4 +259,5 @@ message UpdateDeckConfigsRequest {
bool fsrs = 8; bool fsrs = 8;
bool apply_all_parent_limits = 9; bool apply_all_parent_limits = 9;
bool fsrs_reschedule = 10; bool fsrs_reschedule = 10;
bool fsrs_health_check = 11;
} }

View file

@ -354,11 +354,13 @@ message ComputeFsrsParamsRequest {
repeated float current_params = 2; repeated float current_params = 2;
int64 ignore_revlogs_before_ms = 3; int64 ignore_revlogs_before_ms = 3;
uint32 num_of_relearning_steps = 4; uint32 num_of_relearning_steps = 4;
bool health_check = 5;
} }
message ComputeFsrsParamsResponse { message ComputeFsrsParamsResponse {
repeated float params = 1; repeated float params = 1;
uint32 fsrs_items = 2; uint32 fsrs_items = 2;
optional bool health_check_passed = 3;
} }
message ComputeFsrsParamsFromItemsRequest { message ComputeFsrsParamsFromItemsRequest {

View file

@ -40,6 +40,7 @@ pub enum BoolKey {
WithScheduling, WithScheduling,
WithDeckConfigs, WithDeckConfigs,
Fsrs, Fsrs,
FsrsHealthCheck,
LoadBalancerEnabled, LoadBalancerEnabled,
FsrsShortTermWithStepsEnabled, FsrsShortTermWithStepsEnabled,
#[strum(to_string = "normalize_note_text")] #[strum(to_string = "normalize_note_text")]
@ -76,6 +77,7 @@ impl Collection {
| BoolKey::RestorePositionBrowser | BoolKey::RestorePositionBrowser
| BoolKey::RestorePositionReviewer | BoolKey::RestorePositionReviewer
| BoolKey::LoadBalancerEnabled | BoolKey::LoadBalancerEnabled
| BoolKey::FsrsHealthCheck
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true), | BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),
// other options default to false // other options default to false

View file

@ -192,6 +192,7 @@ impl From<anki_proto::deck_config::UpdateDeckConfigsRequest> for UpdateDeckConfi
apply_all_parent_limits: c.apply_all_parent_limits, apply_all_parent_limits: c.apply_all_parent_limits,
fsrs: c.fsrs, fsrs: c.fsrs,
fsrs_reschedule: c.fsrs_reschedule, fsrs_reschedule: c.fsrs_reschedule,
fsrs_health_check: c.fsrs_health_check,
} }
} }
} }

View file

@ -22,6 +22,7 @@ use crate::prelude::*;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
use crate::scheduler::fsrs::params::ignore_revlogs_before_ms_from_config; use crate::scheduler::fsrs::params::ignore_revlogs_before_ms_from_config;
use crate::scheduler::fsrs::params::ComputeParamsRequest;
use crate::search::JoinSearches; use crate::search::JoinSearches;
use crate::search::Negated; use crate::search::Negated;
use crate::search::SearchNode; use crate::search::SearchNode;
@ -41,6 +42,7 @@ pub struct UpdateDeckConfigsRequest {
pub apply_all_parent_limits: bool, pub apply_all_parent_limits: bool,
pub fsrs: bool, pub fsrs: bool,
pub fsrs_reschedule: bool, pub fsrs_reschedule: bool,
pub fsrs_health_check: bool,
} }
impl Collection { impl Collection {
@ -71,6 +73,7 @@ impl Collection {
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit), new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
apply_all_parent_limits: self.get_config_bool(BoolKey::ApplyAllParentLimits), apply_all_parent_limits: self.get_config_bool(BoolKey::ApplyAllParentLimits),
fsrs: self.get_config_bool(BoolKey::Fsrs), fsrs: self.get_config_bool(BoolKey::Fsrs),
fsrs_health_check: self.get_config_bool(BoolKey::FsrsHealthCheck),
days_since_last_fsrs_optimize, days_since_last_fsrs_optimize,
}) })
} }
@ -300,6 +303,7 @@ impl Collection {
req.new_cards_ignore_review_limit, req.new_cards_ignore_review_limit,
)?; )?;
self.set_config_bool_inner(BoolKey::ApplyAllParentLimits, req.apply_all_parent_limits)?; self.set_config_bool_inner(BoolKey::ApplyAllParentLimits, req.apply_all_parent_limits)?;
self.set_config_bool_inner(BoolKey::FsrsHealthCheck, req.fsrs_health_check)?;
Ok(()) Ok(())
} }
@ -365,14 +369,15 @@ impl Collection {
}; };
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?; let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
let num_of_relearning_steps = config.inner.relearn_steps.len(); let num_of_relearning_steps = config.inner.relearn_steps.len();
match self.compute_params( match self.compute_params(ComputeParamsRequest {
&search, search: &search,
ignore_revlogs_before_ms, ignore_revlogs_before_ms,
idx as u32 + 1, current_preset: idx as u32 + 1,
config_len, total_presets: config_len,
config.fsrs_params(), current_params: config.fsrs_params(),
num_of_relearning_steps, num_of_relearning_steps,
) { health_check: false,
}) {
Ok(params) => { Ok(params) => {
println!("{}: {:?}", config.name, params.params); println!("{}: {:?}", config.name, params.params);
config.inner.fsrs_params_6 = params.params; config.inner.fsrs_params_6 = params.params;
@ -452,6 +457,7 @@ mod test {
col.set_config_string_inner(StringKey::CardStateCustomizer, "")?; col.set_config_string_inner(StringKey::CardStateCustomizer, "")?;
col.set_config_bool_inner(BoolKey::NewCardsIgnoreReviewLimit, false)?; col.set_config_bool_inner(BoolKey::NewCardsIgnoreReviewLimit, false)?;
col.set_config_bool_inner(BoolKey::ApplyAllParentLimits, false)?; col.set_config_bool_inner(BoolKey::ApplyAllParentLimits, false)?;
col.set_config_bool_inner(BoolKey::FsrsHealthCheck, true)?;
// pretend we're in sync // pretend we're in sync
let stamps = col.storage.get_collection_timestamps()?; let stamps = col.storage.get_collection_timestamps()?;
@ -488,6 +494,7 @@ mod test {
apply_all_parent_limits: false, apply_all_parent_limits: false,
fsrs: false, fsrs: false,
fsrs_reschedule: false, fsrs_reschedule: false,
fsrs_health_check: true,
}; };
assert!(!col.update_deck_configs(input.clone())?.changes.had_change()); assert!(!col.update_deck_configs(input.clone())?.changes.had_change());

View file

@ -51,19 +51,46 @@ pub(crate) fn ignore_revlogs_before_ms_from_config(config: &DeckConfig) -> Resul
ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date) ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date)
} }
pub struct ComputeParamsRequest<'t> {
pub search: &'t str,
pub ignore_revlogs_before_ms: TimestampMillis,
pub current_preset: u32,
pub total_presets: u32,
pub current_params: &'t Params,
pub num_of_relearning_steps: usize,
pub health_check: bool,
}
/// r: retention
fn log_loss_adjustment(r: f32) -> f32 {
0.623 * (4. * r * (1. - r)).powf(0.738)
}
/// r: retention
///
/// c: review count
fn rmse_adjustment(r: f32, c: u32) -> f32 {
0.0135 / (r.powf(0.504) - 1.14) + 0.176 / ((c as f32 / 1000.).powf(0.825) + 2.22) + 0.101
}
impl Collection { impl Collection {
/// Note this does not return an error if there are less than 400 items - /// Note this does not return an error if there are less than 400 items -
/// the caller should instead check the fsrs_items count in the return /// the caller should instead check the fsrs_items count in the return
/// value. /// value.
pub fn compute_params( pub fn compute_params(
&mut self, &mut self,
search: &str, request: ComputeParamsRequest,
ignore_revlogs_before: TimestampMillis,
current_preset: u32,
total_presets: u32,
current_params: &Params,
num_of_relearning_steps: usize,
) -> Result<ComputeFsrsParamsResponse> { ) -> Result<ComputeFsrsParamsResponse> {
let ComputeParamsRequest {
search,
ignore_revlogs_before_ms: ignore_revlogs_before,
current_preset,
total_presets,
current_params,
num_of_relearning_steps,
health_check,
} = request;
self.clear_progress(); self.clear_progress();
let timing = self.timing_today()?; let timing = self.timing_today()?;
let revlogs = self.revlog_for_srs(search)?; let revlogs = self.revlog_for_srs(search)?;
@ -75,6 +102,7 @@ impl Collection {
return Ok(ComputeFsrsParamsResponse { return Ok(ComputeFsrsParamsResponse {
params: current_params.to_vec(), params: current_params.to_vec(),
fsrs_items, fsrs_items,
health_check_passed: None,
}); });
} }
// adapt the progress handler to our built-in progress handling // adapt the progress handler to our built-in progress handling
@ -108,12 +136,13 @@ impl Collection {
let (progress, progress_thread) = create_progress_thread()?; let (progress, progress_thread) = create_progress_thread()?;
let fsrs = FSRS::new(None)?; let fsrs = FSRS::new(None)?;
let mut params = fsrs.compute_parameters(ComputeParametersInput { let input = ComputeParametersInput {
train_set: items.clone(), train_set: items.clone(),
progress: Some(progress.clone()), progress: Some(progress.clone()),
enable_short_term: true, enable_short_term: true,
num_relearning_steps: Some(num_of_relearning_steps), num_relearning_steps: Some(num_of_relearning_steps),
})?; };
let mut params = fsrs.compute_parameters(input.clone())?;
progress_thread.join().ok(); progress_thread.join().ok();
if let Ok(current_fsrs) = FSRS::new(Some(current_params)) { if let Ok(current_fsrs) = FSRS::new(Some(current_params)) {
let current_log_loss = current_fsrs.evaluate(items.clone(), |_| true)?.log_loss; let current_log_loss = current_fsrs.evaluate(items.clone(), |_| true)?.log_loss;
@ -145,7 +174,34 @@ impl Collection {
} }
} }
Ok(ComputeFsrsParamsResponse { params, fsrs_items }) let health_check_passed = if health_check {
let fsrs = FSRS::new(None)?;
fsrs.evaluate_with_time_series_splits(input, |_| true)
.ok()
.map(|eval| {
let r = items.iter().fold(0, |p, item| {
p + (item
.reviews
.last()
.map(|reviews| reviews.rating)
.unwrap_or(0)
> 1) as u32
}) as f32
/ fsrs_items as f32;
let adjusted_log_loss = eval.log_loss / log_loss_adjustment(r);
let adjusted_rmse = eval.rmse_bins / rmse_adjustment(r, fsrs_items);
adjusted_log_loss <= 1.11 || adjusted_rmse <= 1.53
})
} else {
None
};
Ok(ComputeFsrsParamsResponse {
params,
fsrs_items,
health_check_passed,
})
} }
pub(crate) fn revlog_for_srs( pub(crate) fn revlog_for_srs(

View file

@ -23,6 +23,7 @@ use fsrs::FSRS;
use crate::backend::Backend; use crate::backend::Backend;
use crate::prelude::*; use crate::prelude::*;
use crate::scheduler::fsrs::params::ComputeParamsRequest;
use crate::scheduler::new::NewCardDueOrder; use crate::scheduler::new::NewCardDueOrder;
use crate::scheduler::states::CardState; use crate::scheduler::states::CardState;
use crate::scheduler::states::SchedulingStates; use crate::scheduler::states::SchedulingStates;
@ -264,14 +265,15 @@ impl crate::services::SchedulerService for Collection {
&mut self, &mut self,
input: scheduler::ComputeFsrsParamsRequest, input: scheduler::ComputeFsrsParamsRequest,
) -> Result<scheduler::ComputeFsrsParamsResponse> { ) -> Result<scheduler::ComputeFsrsParamsResponse> {
self.compute_params( self.compute_params(ComputeParamsRequest {
&input.search, search: &input.search,
input.ignore_revlogs_before_ms.into(), ignore_revlogs_before_ms: input.ignore_revlogs_before_ms.into(),
1, current_preset: 1,
1, total_presets: 1,
&input.current_params, current_params: &input.current_params,
input.num_of_relearning_steps as usize, num_of_relearning_steps: input.num_of_relearning_steps as usize,
) health_check: input.health_check,
})
} }
fn simulate_fsrs_review( fn simulate_fsrs_review(
@ -372,7 +374,11 @@ impl crate::services::BackendSchedulerService for Backend {
enable_short_term: true, enable_short_term: true,
num_relearning_steps: None, num_relearning_steps: None,
})?; })?;
Ok(ComputeFsrsParamsResponse { params, fsrs_items }) Ok(ComputeFsrsParamsResponse {
params,
fsrs_items,
health_check_passed: None,
})
} }
fn fsrs_benchmark( fn fsrs_benchmark(

View file

@ -58,6 +58,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let computingParams = false; let computingParams = false;
let checkingParams = false; let checkingParams = false;
const healthCheck = state.fsrsHealthCheck;
$: computing = computingParams || checkingParams; $: computing = computingParams || checkingParams;
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`; $: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
$: roundedRetention = Number($config.desiredRetention.toFixed(2)); $: roundedRetention = Number($config.desiredRetention.toFixed(2));
@ -178,6 +180,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
currentParams: params, currentParams: params,
numOfRelearningSteps: numOfRelearningStepsInDay, numOfRelearningSteps: numOfRelearningStepsInDay,
healthCheck: $healthCheck,
}); });
const already_optimal = const already_optimal =
@ -187,12 +190,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
)) || )) ||
resp.params.length === 0; resp.params.length === 0;
if (already_optimal) { if (resp.healthCheckPassed !== undefined) {
if (resp.healthCheckPassed) {
setTimeout(() => alert(tr.deckConfigFsrsGoodFit()), 200);
} else {
setTimeout(
() => alert(tr.deckConfigFsrsBadFitWarning()),
200,
);
}
} else if (already_optimal) {
const msg = resp.fsrsItems const msg = resp.fsrsItems
? tr.deckConfigFsrsParamsOptimal() ? tr.deckConfigFsrsParamsOptimal()
: tr.deckConfigFsrsParamsNoReviews(); : tr.deckConfigFsrsParamsNoReviews();
setTimeout(() => alert(msg), 200); setTimeout(() => alert(msg), 200);
} else { }
if (!already_optimal) {
$config.fsrsParams6 = resp.params; $config.fsrsParams6 = resp.params;
optimized = true; optimized = true;
} }
@ -312,6 +325,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
placeholder={defaultparamSearch} placeholder={defaultparamSearch}
/> />
<SwitchRow bind:value={$fsrsReschedule} defaultValue={false}>
<SettingTitle on:click={() => openHelpModal("rescheduleCardsOnChange")}>
<GlobalLabel title={tr.deckConfigRescheduleCardsOnChange()} />
</SettingTitle>
</SwitchRow>
{#if $fsrsReschedule}
<Warning warning={tr.deckConfigRescheduleCardsWarning()} />
{/if}
<SwitchRow bind:value={$healthCheck} defaultValue={false}>
<SettingTitle on:click={() => openHelpModal("deckConfigHealthCheck")}>
<GlobalLabel title={tr.deckConfigHealthCheck()} />
</SettingTitle>
</SwitchRow>
<button <button
class="btn {computingParams ? 'btn-warning' : 'btn-primary'}" class="btn {computingParams ? 'btn-warning' : 'btn-primary'}"
disabled={!computingParams && computing} disabled={!computingParams && computing}
@ -323,6 +352,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{tr.deckConfigOptimizeButton()} {tr.deckConfigOptimizeButton()}
{/if} {/if}
</button> </button>
{#if false}
<!-- Can be re-enabled by some method in the future -->
<button <button
class="btn {checkingParams ? 'btn-warning' : 'btn-primary'}" class="btn {checkingParams ? 'btn-warning' : 'btn-primary'}"
disabled={!checkingParams && computing} disabled={!checkingParams && computing}
@ -334,6 +365,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{tr.deckConfigEvaluateButton()} {tr.deckConfigEvaluateButton()}
{/if} {/if}
</button> </button>
{/if}
<div> <div>
{#if computingParams || checkingParams} {#if computingParams || checkingParams}
{computeParamsProgressString} {computeParamsProgressString}
@ -351,18 +383,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</button> </button>
</div> </div>
<div class="m-2">
<SwitchRow bind:value={$fsrsReschedule} defaultValue={false}>
<SettingTitle on:click={() => openHelpModal("rescheduleCardsOnChange")}>
<GlobalLabel title={tr.deckConfigRescheduleCardsOnChange()} />
</SettingTitle>
</SwitchRow>
{#if $fsrsReschedule}
<Warning warning={tr.deckConfigRescheduleCardsWarning()} />
{/if}
</div>
<div class="m-2"> <div class="m-2">
<button class="btn btn-primary" on:click={() => (showSimulator = true)}> <button class="btn btn-primary" on:click={() => (showSimulator = true)}>
{tr.deckConfigFsrsSimulatorExperimental()} {tr.deckConfigFsrsSimulatorExperimental()}

View file

@ -62,6 +62,7 @@ export class DeckOptionsState {
readonly applyAllParentLimits: Writable<boolean>; readonly applyAllParentLimits: Writable<boolean>;
readonly fsrs: Writable<boolean>; readonly fsrs: Writable<boolean>;
readonly fsrsReschedule: Writable<boolean> = writable(false); readonly fsrsReschedule: Writable<boolean> = writable(false);
readonly fsrsHealthCheck: Writable<boolean>;
readonly daysSinceLastOptimization: Writable<number>; readonly daysSinceLastOptimization: Writable<number>;
readonly currentPresetName: Writable<string>; readonly currentPresetName: Writable<string>;
/** Used to detect if there are any pending changes */ /** Used to detect if there are any pending changes */
@ -103,6 +104,7 @@ export class DeckOptionsState {
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit); this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
this.applyAllParentLimits = writable(data.applyAllParentLimits); this.applyAllParentLimits = writable(data.applyAllParentLimits);
this.fsrs = writable(data.fsrs); this.fsrs = writable(data.fsrs);
this.fsrsHealthCheck = writable(data.fsrsHealthCheck);
this.daysSinceLastOptimization = writable(data.daysSinceLastFsrsOptimize); this.daysSinceLastOptimization = writable(data.daysSinceLastFsrsOptimize);
// decrement the use count of the starting item, as we'll apply +1 to currently // decrement the use count of the starting item, as we'll apply +1 to currently
@ -267,6 +269,7 @@ export class DeckOptionsState {
applyAllParentLimits: get(this.applyAllParentLimits), applyAllParentLimits: get(this.applyAllParentLimits),
fsrs: get(this.fsrs), fsrs: get(this.fsrs),
fsrsReschedule: get(this.fsrsReschedule), fsrsReschedule: get(this.fsrsReschedule),
fsrsHealthCheck: get(this.fsrsHealthCheck),
}; };
} }