Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (C) 2022 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foundry.gradle.util
import com.squareup.moshi.JsonClass
import com.sun.jna.Library
import com.sun.jna.Native
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
import foundry.cli.AppleSiliconCompat.Arch
import foundry.cli.AppleSiliconCompat.Arch.ARM64
import foundry.cli.AppleSiliconCompat.Arch.X86_64
import foundry.gradle.util.AppleSiliconThermals.ThermalState.CRITICAL
import foundry.gradle.util.AppleSiliconThermals.ThermalState.FAIR
import foundry.gradle.util.AppleSiliconThermals.ThermalState.NOMINAL
import foundry.gradle.util.AppleSiliconThermals.ThermalState.SERIOUS
import foundry.gradle.util.charting.ChartCreator
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.observers.DisposableObserver
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import java.io.File
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit.SECONDS
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.Throws
import org.gradle.api.logging.Logger
/**
* Simple interface for recording thermals data during the course of a build. The watcher should be
* [started][start] when the build is started and [stopped][stop] when the build is complete.
*
* Current thermals data can be [peeked][peek] at at any time.
*/
// Can't be a sealed interface because Gradle hasn't figured out Kotlin 1.5 yet.
internal interface ThermalsWatcher {
fun start(executor: Executor)
fun peek(): Thermals
fun stop(): Thermals
companion object {
operator fun invoke(logger: Logger, thermalsFileProvider: () -> File): ThermalsWatcher {
return when (val arch = Arch.get()) {
ARM64 -> AppleSiliconThermals(logger, thermalsFileProvider)
X86_64 -> IntelThermalsParser(thermalsFileProvider)
else -> error("Unsupported architecture: $arch")
}
}
}
}
internal class IntelThermalsParser(val thermalsFileProvider: () -> File) : ThermalsWatcher {
private var process: Process? = null
private var thermalsFile: File? = null
private val started = AtomicBoolean(false)
override fun start(executor: Executor) {
check(!started.getAndSet(true)) { "Already started" }
thermalsFile = thermalsFileProvider()
process = ProcessBuilder("pmset", "-g", "thermlog").redirectOutput(thermalsFile).start()
}
override fun peek(): Thermals {
check(started.get()) { "Not started" }
val file = thermalsFile ?: return Thermals.Empty
// File may no longer exist if they were running `clean`
return if (file.exists()) {
val thermalsLog = file.readText()
ThermlogParser.parse(thermalsLog)
} else {
Thermals.Empty
}
}
override fun stop(): Thermals {
check(started.get()) { "Not started" }
val thermals =
try {
peek()
} finally {
process?.destroyForcibly()?.waitFor()
}
started.set(false)
process = null
thermalsFile = null
return thermals
}
}
/** Utility for parsing thermals as reported by `pmset -g thermlog`. */
internal object ThermlogParser {
internal val TIMESTAMP_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")
internal fun parse(log: String): Thermals {
return parseLogs(log)?.let(Thermals.Companion::create) ?: Thermals.Empty
}
private fun parseLogs(log: String): List? {
val logs = mutableListOf()
var currentTime: LocalDateTime? = null
var currentScheduler: Int? = null
var availableCpus: Int? = null
var speedLimit: Int? = null
fun flush() {
val prevCurrentTime = currentTime
val prevCurrentScheduler = currentScheduler
val prevAvailableCpus = availableCpus
val prevSpeedLimit = speedLimit
if (
prevCurrentTime != null &&
prevCurrentScheduler != null &&
prevAvailableCpus != null &&
prevSpeedLimit != null
) {
logs += ThermLog(prevCurrentTime, prevCurrentScheduler, prevAvailableCpus, prevSpeedLimit)
}
}
for (line in log.lineSequence()) {
when {
line.isBlank() -> continue
"CPU Power notify" in line -> {
// Flush the previous data
flush()
currentTime = null
currentScheduler = null
availableCpus = null
speedLimit = null
val localCurrentTime =
try {
parseTimestamp(line)
} catch (e: DateTimeParseException) {
// Sometimes the process falls over piping data out, so gracefully skip this one
null
}
currentTime = localCurrentTime
}
currentTime != null && currentScheduler == null && "CPU_Scheduler_Limit" in line -> {
currentScheduler = parseDiagnostic(line)
}
currentTime != null && availableCpus == null && "CPU_Available_CPUs" in line -> {
availableCpus = parseDiagnostic(line)
}
currentTime != null && speedLimit == null && "CPU_Speed_Limit" in line -> {
speedLimit = parseDiagnostic(line)
}
else -> {
// Skip it, unrecognized text
}
}
}
// flush any remaining
flush()
return logs.takeIf { it.isNotEmpty() }
}
private fun parseDiagnostic(line: String): Int {
// CPU_Available_CPUs = 8
return line.substringAfter("=").trim().toInt()
}
@Throws(DateTimeParseException::class)
private fun parseTimestamp(line: String): LocalDateTime {
// 2021-07-07 12:32:50 -0400 CPU Power notify
return LocalDateTime.parse(line.substringBefore("CPU").trim(), TIMESTAMP_PATTERN)
}
}
@JsonClass(generateAdapter = true, generator = "sealed:type")
public sealed class Thermals {
public abstract val wasThrottled: Boolean
@TypeLabel("empty")
public object Empty : Thermals() {
override val wasThrottled: Boolean = false
}
internal companion object {
fun create(logs: List): Thermals {
return if (logs.isEmpty()) {
Empty
} else {
ThermalsData(logs)
}
}
}
}
@TypeLabel("data")
@JsonClass(generateAdapter = true)
public data class ThermalsData(public val logs: List) : Thermals() {
init {
check(logs.isNotEmpty())
}
override val wasThrottled: Boolean by lazy { logs.any { it.isThrottled } }
public val lowest: Int by lazy { logs.minOf { it.speedLimit } }
public val average: Int by lazy { logs.sumOf { it.speedLimit } / logs.size }
public val percentThrottled: Int by lazy {
((logs.count { it.speedLimit < 100 }.toDouble() / logs.size) * 100).toInt()
}
public val allSpeedLimits: List by lazy { logs.map { it.speedLimit } }
/** Returns a url to a chart representation of the thermals logged. */
public fun chartUrl(urlCharLimit: Int): String {
require(urlCharLimit >= MINIMUM_CHAR_LIMIT) {
"Input limit $urlCharLimit is below the minimum of $MINIMUM_CHAR_LIMIT."
}
// x axis is the duration of the build in seconds
val xLength = Duration.between(logs.first().timestamp, logs.last().timestamp).seconds.toInt()
val xRange = 0..xLength
val yRange = 0..100
return generateSequence(1) { it + 1 }
.map { sampleSize ->
val sampleValues = allSpeedLimits.sample(sampleSize)
// The data is sampled but the x axis should still use labels for the full range of time.
ChartCreator.createChartUrl(data = sampleValues, xRange = xRange, yRange = yRange)
}
.first { it.length < urlCharLimit }
}
private companion object {
/** Keep the minimum relatively high to avoid work in sampling data. */
const val MINIMUM_CHAR_LIMIT = 1000
fun List.sample(sampleSize: Int): List {
return filterIndexed { index, _ -> index % sampleSize == 0 }
}
}
}
@JsonClass(generateAdapter = true)
public data class ThermLog(
@Suppress("MoshiUsageNonMoshiClassPlatform") public val timestamp: LocalDateTime,
public val schedulerLimit: Int,
public val availableCpus: Int,
public val speedLimit: Int,
) {
val isThrottled: Boolean = speedLimit != -1 && speedLimit < 100
}
internal class AppleSiliconThermals(
private val logger: Logger,
private val thermalsFileProvider: () -> File,
) : ThermalsWatcher {
private var emissions: BehaviorSubject = BehaviorSubject.create()
private var heartbeat: Disposable? = null
private var thermalsFile: File? = null
private val started = AtomicBoolean(false)
override fun start(executor: Executor) {
check(!started.getAndSet(true)) { "Already started." }
thermalsFile = thermalsFileProvider()
heartbeat =
Observable.interval(5, SECONDS, Schedulers.from(executor))
.map {
ThermLog(
timestamp = LocalDateTime.now(),
schedulerLimit = 0,
availableCpus = 0,
// These aren't entirely true numbers but best effort for now
speedLimit =
when (getThermalState()) {
NOMINAL -> 100
FAIR -> 75
SERIOUS -> 50
CRITICAL -> 25
null -> -1
},
)
}
// TODO we should try to better understand this issue, but for now let's stop trying to
// track if it fails.
.takeUntil { it.speedLimit == -1 }
.doOnNext { thermalsFile?.appendText("\n${it.timestamp} - ${it.speedLimit}") }
.scanWith({ Thermals.Empty }) { acc, next ->
when (acc) {
is Thermals.Empty -> ThermalsData(listOf(next))
is ThermalsData -> ThermalsData(acc.logs + next)
}
}
.subscribeWith(
object : DisposableObserver() {
override fun onNext(thermals: Thermals) {
emissions.onNext(thermals)
}
override fun onError(e: Throwable) {
logger.error("Error in thermals watcher:\n${e.stackTraceToString()}")
}
override fun onComplete() {
// If we completed then we received an unknown state.
logger.error("Could not read thermals for this build.")
}
}
)
}
override fun peek(): Thermals {
check(started.get()) { "Not started." }
return emissions.value ?: Thermals.Empty
}
override fun stop(): Thermals {
check(started.getAndSet(false)) { "Not started." }
heartbeat?.dispose()
heartbeat = null
thermalsFile = null
val thermals = emissions.value ?: Thermals.Empty
emissions = BehaviorSubject.create()
return thermals
}
private fun getThermalState(): ThermalState? {
val rawValue =
try {
JnaThermalState.INSTANCE.thermal_state()
} catch (t: Throwable) {
// If Gradle is misconfigured, this will fail to load and result in a massive cascading
// stacktrace to RxJava that's more or less incomprehensible to read. Instead, we'll
// just swallow the exception and log an error.
logger.error("Could not read thermals state: ${t.message}")
return null
}
// Raw value in this case is 0-4, which we use as the ordinal of our enums for convenience
return if (rawValue in 0..ThermalState.entries.size) {
ThermalState.entries[rawValue]
} else {
null
}
}
/** From `Foundation.NSProcessInfo.ThermalState`. */
enum class ThermalState(@Suppress("unused") val rawValue: Int) {
/** No corrective action is needed. */
NOMINAL(0),
/**
* The system has reached a state where fans may become audible (on systems which have fans).
* Recommendation: Defer non-user-visible activity.
*/
FAIR(1),
/**
* Fans are running at maximum speed (on systems which have fans), system performance may be
* impacted. Recommendation: reduce application's usage of CPU, GPU and I/O, if possible. Switch
* to lower quality visual effects, reduce frame rates.
*/
SERIOUS(2),
/**
* System performance is significantly impacted and the system needs to cool down.
* Recommendation: reduce application's usage of CPU, GPU, and I/O to the minimum level needed
* to respond to user actions. Consider stopping use of camera and other peripherals if your
* application is using them.
*/
CRITICAL(3),
}
}
/** JNA wrapper interface for the `thermal_state` dylib */
internal interface JnaThermalState : Library {
// Corresponds to the thermal_state function in thermal_state.swift
@Suppress("FunctionName") fun thermal_state(): Int
companion object {
val INSTANCE: JnaThermalState =
Native.load("thermal_state", JnaThermalState::class.java) as JnaThermalState
}
}