Skip to content

Commit 9849649

Browse files
authored
Merge pull request #3384 from mtomko/starvation-clock
Starvation clock timestamp
2 parents 7f23462 + 6d46203 commit 9849649

6 files changed

Lines changed: 86 additions & 19 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2020-2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cats.effect
18+
19+
import scala.concurrent.duration.FiniteDuration
20+
import scala.scalajs.js
21+
22+
private[effect] abstract class CpuStarvationCheckPlatform { this: CpuStarvationCheck.type =>
23+
24+
def format(when: FiniteDuration): String =
25+
new js.Date(when.toMillis.toDouble).toISOString()
26+
27+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2020-2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cats.effect
18+
19+
import scala.concurrent.duration.FiniteDuration
20+
21+
import java.time.Instant
22+
import java.time.format.DateTimeFormatter
23+
24+
private[effect] abstract class CpuStarvationCheckPlatform { this: CpuStarvationCheck.type =>
25+
26+
def format(when: FiniteDuration): String =
27+
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(when.toMillis))
28+
29+
}

core/shared/src/main/scala/cats/effect/CpuStarvationCheck.scala

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,36 @@ import cats.effect.unsafe.IORuntimeConfig
2222
import cats.syntax.all._
2323

2424
import scala.concurrent.duration.{Duration, FiniteDuration}
25-
26-
private[effect] object CpuStarvationCheck {
25+
private[effect] object CpuStarvationCheck extends CpuStarvationCheckPlatform {
2726

2827
def run(runtimeConfig: IORuntimeConfig, metrics: CpuStarvationMetrics): IO[Nothing] = {
2928
import runtimeConfig._
3029

3130
val threshold = cpuStarvationCheckInterval * (1 + cpuStarvationCheckThreshold)
3231

33-
val warning = mkWarning(cpuStarvationCheckInterval * cpuStarvationCheckThreshold)
32+
val warning: FiniteDuration => String =
33+
mkWarning(cpuStarvationCheckInterval * cpuStarvationCheckThreshold)
3434

3535
def go(initial: FiniteDuration): IO[Nothing] =
3636
IO.sleep(cpuStarvationCheckInterval) >> IO.monotonic.flatMap { now =>
3737
val delta = now - initial
3838

3939
metrics.recordClockDrift(delta - cpuStarvationCheckInterval) >>
40-
(Console[IO].errorln(warning) *> metrics.incCpuStarvationCount)
41-
.whenA(delta >= threshold) >>
42-
go(now)
40+
IO.realTime
41+
.flatMap(fd =>
42+
(Console[IO].errorln(warning(fd)) *> metrics.incCpuStarvationCount)
43+
.whenA(delta >= threshold)) >> go(now)
4344
}
4445

4546
IO.monotonic.flatMap(go(_)).delayBy(cpuStarvationCheckInitialDelay)
4647
}
4748

48-
private[this] def mkWarning(threshold: Duration) =
49-
s"""|[WARNING] Your app's responsiveness to a new asynchronous event (such as a
50-
|new connection, an upstream response, or a timer) was in excess of $threshold.
51-
|Your CPU is probably starving. Consider increasing the granularity
52-
|of your delays or adding more cedes. This may also be a sign that you are
53-
|unintentionally running blocking I/O operations (such as File or InetAddress)
49+
private[this] def mkWarning(threshold: Duration)(when: FiniteDuration) =
50+
s"""|${format(when)} [WARNING] Your app's responsiveness to a new asynchronous
51+
|event (such as a new connection, an upstream response, or a timer) was in excess
52+
|of $threshold. Your CPU is probably starving. Consider increasing the
53+
|granularity of your delays or adding more cedes. This may also be a sign that you
54+
|are unintentionally running blocking I/O operations (such as File or InetAddress)
5455
|without the blocking combinator.""".stripMargin
5556

5657
}

docs/core/starvation-and-tuning.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ id: starvation-and-tuning
33
title: Starvation and Tuning
44
---
55

6-
All Cats Effect applications constructed via `IOApp` have an automatic mechanism which periodically checks to see if the application runtime is starving for compute resources. If you ever see warnings which look like the following, they are the result of this mechanism automatically detecting that the responsiveness of your application runtime is below the configured threshold.
6+
All Cats Effect applications constructed via `IOApp` have an automatic mechanism which periodically checks to see if the application runtime is starving for compute resources. If you ever see warnings which look like the following, they are the result of this mechanism automatically detecting that the responsiveness of your application runtime is below the configured threshold. Note that the timestamp is the time when the starvation was detected, which is not precisely the time when starvation (or the task that is responsible) began.
77

88
```
9-
[WARNING] Your CPU is probably starving. Consider increasing the granularity
10-
of your delays or adding more cedes. This may also be a sign that you are
11-
unintentionally running blocking I/O operations (such as File or InetAddress)
9+
2023-01-28T00:16:24.101Z [WARNING] Your app's responsiveness to a new asynchronous
10+
event (such as a new connection, an upstream response, or a timer) was in excess
11+
of 40 milliseconds. Your CPU is probably starving. Consider increasing the
12+
granularity of your delays or adding more cedes. This may also be a sign that you
13+
are unintentionally running blocking I/O operations (such as File or InetAddress)
1214
without the blocking combinator.
1315
```
1416

docs/faq.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,11 @@ Note that this is just a specific example to demonstrate why running an non-bloc
7474
Cats Effect 3.4.0 introduced a default-enabled *starvation checker*, which produces warnings like the following:
7575

7676
```
77-
[WARNING] Your CPU is probably starving. Consider increasing the granularity
78-
of your delays or adding more cedes. This may also be a sign that you are
79-
unintentionally running blocking I/O operations (such as File or InetAddress)
77+
2023-01-28T00:16:24.101Z [WARNING] Your app's responsiveness to a new asynchronous
78+
event (such as a new connection, an upstream response, or a timer) was in excess
79+
of 40 milliseconds. Your CPU is probably starving. Consider increasing the
80+
granularity of your delays or adding more cedes. This may also be a sign that you
81+
are unintentionally running blocking I/O operations (such as File or InetAddress)
8082
without the blocking combinator.
8183
```
8284

tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,12 @@ class IOAppSpec extends Specification {
201201
val err = h.stderr()
202202
err must not(contain("[WARNING] Failed to register Cats Effect CPU"))
203203
err must contain("[WARNING] Your app's responsiveness")
204+
// we use a regex because time has too many corner cases - a test run at just the wrong
205+
// moment on new year's eve, etc
206+
err must beMatching(
207+
// (?s) allows matching across line breaks
208+
"""(?s)^\d{4}-[01]\d-[0-3]\dT[012]\d:[0-6]\d:[0-6]\d(?:\.\d{1,3})?Z \[WARNING\] Your app's responsiveness.*"""
209+
)
204210
}
205211

206212
"custom runtime installed as global" in {

0 commit comments

Comments
 (0)