Skip to content

Commit

Permalink
feat: Added Performance Metrics for Android (#1292)
Browse files Browse the repository at this point in the history
* feat: Added Performance Metrics for Android

* Add tests

* Add tests
  • Loading branch information
piotradamczyk5 authored Nov 10, 2020
1 parent ab55fbc commit 68d52d8
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 20 deletions.
17 changes: 17 additions & 0 deletions test_runner/src/main/kotlin/ftl/gc/GcStorage.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ftl.gc

import com.google.api.client.http.GoogleApiLogger
import com.google.api.services.toolresults.model.PerfMetricsSummary
import com.google.cloud.storage.BlobInfo
import com.google.cloud.storage.Storage
import com.google.cloud.storage.Storage.BlobListOption.pageSize
Expand Down Expand Up @@ -84,6 +85,22 @@ object GcStorage {
}
}

fun uploadPerformanceMetrics(perfMetricsSummary: PerfMetricsSummary, resultsBucket: String, resultDir: String) {
val performanceMetricsFileName = "performanceMetrics.json"
runCatching {
upload(
performanceMetricsFileName,
perfMetricsSummary.toPrettyString().toByteArray(),
resultsBucket,
resultDir
)
}.onFailure {
println("Cannot upload performance metrics ${it.message}")
}.onSuccess {
println("Performance metrics uploaded to https://console.developers.google.com/storage/browser/$resultsBucket/$resultDir")
}
}

fun uploadReportResult(testResult: String, args: IArgs, fileName: String) {
if (args.resultsBucket.isBlank() || args.resultsDir.isBlank() || args.disableResultsUpload) return
upload(
Expand Down
15 changes: 15 additions & 0 deletions test_runner/src/main/kotlin/ftl/gc/GcToolResults.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ object GcToolResults {
.executeWithRetry()
}

fun getPerformanceMetric(
toolResultsStep: ToolResultsStep
) = service
.projects()
.histories()
.executions()
.steps()
.getPerfMetricsSummary(
toolResultsStep.projectId,
toolResultsStep.historyId,
toolResultsStep.executionId,
toolResultsStep.stepId
)
.executeWithRetry()

// Lists Test Cases attached to a Step
fun listTestCases(toolResultsStep: ToolResultsStep): ListTestCasesResponse {
return service
Expand Down
28 changes: 28 additions & 0 deletions test_runner/src/main/kotlin/ftl/mock/MockServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ import com.google.api.services.testing.model.TestExecution
import com.google.api.services.testing.model.TestMatrix
import com.google.api.services.testing.model.ToolResultsExecution
import com.google.api.services.testing.model.ToolResultsStep
import com.google.api.services.toolresults.model.AppStartTime
import com.google.api.services.toolresults.model.CPUInfo
import com.google.api.services.toolresults.model.Duration
import com.google.api.services.toolresults.model.EnvironmentDimensionValueEntry
import com.google.api.services.toolresults.model.FailureDetail
import com.google.api.services.toolresults.model.GraphicsStats
import com.google.api.services.toolresults.model.History
import com.google.api.services.toolresults.model.InconclusiveDetail
import com.google.api.services.toolresults.model.ListEnvironmentsResponse
import com.google.api.services.toolresults.model.ListHistoriesResponse
import com.google.api.services.toolresults.model.ListStepsResponse
import com.google.api.services.toolresults.model.MemoryInfo
import com.google.api.services.toolresults.model.MergedResult
import com.google.api.services.toolresults.model.Outcome
import com.google.api.services.toolresults.model.PerfEnvironment
import com.google.api.services.toolresults.model.PerfMetricsSummary
import com.google.api.services.toolresults.model.ProjectSettings
import com.google.api.services.toolresults.model.SkippedDetail
import com.google.api.services.toolresults.model.Step
Expand Down Expand Up @@ -334,6 +340,28 @@ object MockServer {
println("Unknown GET " + call.request.uri)
call.respond("")
}

get("/toolresults/v1beta3/projects/{projectId}/histories/{historyId}/executions/{executionId}/steps/{stepId}/perfMetricsSummary") {
val performanceMetricsSummary = PerfMetricsSummary()
.setStepId(call.parameters["stepId"])
.setHistoryId(call.parameters["historyId"])
.setProjectId(call.parameters["projectId"])
.setExecutionId(call.parameters["executionId"])
.setAppStartTime(
AppStartTime()
.setInitialDisplayTime(Duration().setSeconds(4))
.setFullyDrawnTime(Duration().setSeconds(5))
)
.setPerfEnvironment(
PerfEnvironment()
.setCpuInfo(CPUInfo().setCpuProcessor("mock_processor").setNumberOfCores(16))
.setMemoryInfo(MemoryInfo().setMemoryCapInKibibyte(1024))
)
.setGraphicsStats(
GraphicsStats().setP90Millis(100)
)
call.respond(performanceMetricsSummary)
}
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions test_runner/src/main/kotlin/ftl/reports/api/PerformanceMetrics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ftl.reports.api

import com.google.api.services.testing.model.TestExecution
import com.google.api.services.toolresults.model.PerfMetricsSummary
import ftl.android.AndroidCatalog
import ftl.gc.GcStorage
import ftl.gc.GcToolResults
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking

internal fun List<Pair<TestExecution, String>>.getAndUploadPerformanceMetrics(
resultBucket: String
) = runBlocking {
filterAndroidPhysicalDevicesRuns()
.map { (testExecution, gcsStoragePath) ->
async(Dispatchers.IO) {
testExecution.getPerformanceMetric().upload(resultBucket = resultBucket, resultDir = gcsStoragePath)
}
}
.awaitAll()
}

private fun List<Pair<TestExecution, String>>.filterAndroidPhysicalDevicesRuns() = filterNot { (testExecution, _) ->
AndroidCatalog.isVirtualDevice(testExecution.environment.androidDevice, testExecution.projectId)
}

private fun TestExecution.getPerformanceMetric() = GcToolResults.getPerformanceMetric(toolResultsStep)

private fun PerfMetricsSummary.upload(
resultBucket: String,
resultDir: String
) = GcStorage.uploadPerformanceMetrics(this, resultBucket, resultDir)
10 changes: 1 addition & 9 deletions test_runner/src/main/kotlin/ftl/reports/api/ProcessFromApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,12 @@ import com.google.api.services.testing.model.TestMatrix
import ftl.args.IArgs
import ftl.gc.GcTestMatrix
import ftl.json.MatrixMap
import ftl.reports.xml.model.JUnitTestResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking

fun processXmlFromApi(
matrices: MatrixMap,
args: IArgs,
withStackTraces: Boolean = false
): JUnitTestResult = refreshMatricesAndGetExecutions(matrices, args)
.createJUnitTestResult(withStackTraces)

private fun refreshMatricesAndGetExecutions(matrices: MatrixMap, args: IArgs): List<TestExecution> = refreshTestMatrices(
fun refreshMatricesAndGetExecutions(matrices: MatrixMap, args: IArgs): List<TestExecution> = refreshTestMatrices(
matrixIds = matrices.map.values.map { it.matrixId },
projectId = args.project
).getTestExecutions()
Expand Down
59 changes: 48 additions & 11 deletions test_runner/src/main/kotlin/ftl/reports/util/ReportManager.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ftl.reports.util

import com.google.api.services.testing.model.TestExecution
import com.google.common.annotations.VisibleForTesting
import ftl.args.AndroidArgs
import ftl.args.IArgs
import ftl.args.IgnoredTestCases
import ftl.args.IosArgs
Expand All @@ -13,7 +15,9 @@ import ftl.reports.FullJUnitReport
import ftl.reports.HtmlErrorReport
import ftl.reports.JUnitReport
import ftl.reports.MatrixResultsReport
import ftl.reports.api.processXmlFromApi
import ftl.reports.api.getAndUploadPerformanceMetrics
import ftl.reports.api.createJUnitTestResult
import ftl.reports.api.refreshMatricesAndGetExecutions
import ftl.reports.xml.model.JUnitTestCase
import ftl.reports.xml.model.JUnitTestResult
import ftl.reports.xml.model.getSkippedJUnitTestSuite
Expand Down Expand Up @@ -79,7 +83,7 @@ object ReportManager {
// ios supports only legacy parsing
args is IosArgs -> processXmlFromFile(matrices, args, ::parseAllSuitesXml)
args.useLegacyJUnitResult -> processXmlFromFile(matrices, args, ::parseOneSuiteXml)
else -> processXmlFromApi(matrices, args)
else -> refreshMatricesAndGetExecutions(matrices, args).createJUnitTestResult()
}

/** Returns true if there were no test failures */
Expand All @@ -94,12 +98,8 @@ object ReportManager {
val useFlakyTests = args.flakyTestAttempts > 0
if (useFlakyTests) JUnitDedupe.modify(testSuite)
}
listOf(
CostReport,
MatrixResultsReport
).map {
it.run(matrices, testSuite, printToStdout = true, args = args)
}
listOf(CostReport, MatrixResultsReport)
.map { it.run(matrices, testSuite, printToStdout = true, args = args) }

if (!matrices.isAllSuccessful()) {
listOf(
Expand All @@ -116,13 +116,45 @@ object ReportManager {
printToStdout = false,
args = args
)

val testExecutions = refreshMatricesAndGetExecutions(matrices, args)
processJunitResults(args, matrices, testSuite, testShardChunks, testExecutions)
createAndUploadPerformanceMetricsForAndroid(args, testExecutions, matrices)
}

private fun processJunitResults(
args: IArgs,
matrices: MatrixMap,
testSuite: JUnitTestResult?,
testShardChunks: ShardChunks,
testExecutions: List<TestExecution>
) {
when {
args.fullJUnitResult -> processFullJunitResult(args, matrices, testShardChunks)
args.fullJUnitResult -> processFullJunitResult(args, matrices, testShardChunks, testExecutions)
args.useLegacyJUnitResult -> processJunitXml(testSuite, args, testShardChunks)
else -> processJunitXml(testSuite, args, testShardChunks)
}
}

private fun createAndUploadPerformanceMetricsForAndroid(
args: IArgs,
testExecutions: List<TestExecution>,
matrices: MatrixMap
) {
testExecutions
.takeIf { args is AndroidArgs }
?.run {
withGcsStoragePath(matrices, args.resultsDir).getAndUploadPerformanceMetrics(args.resultsBucket)
}
}

private fun List<TestExecution>.withGcsStoragePath(
matrices: MatrixMap,
defaultResultDir: String
) = map { testExecution ->
testExecution to (matrices.map[testExecution.matrixId]?.gcsPathWithoutRootBucket ?: defaultResultDir)
}

private fun IgnoredTestCases.toJunitTestsResults() = getSkippedJUnitTestSuite(
map {
JUnitTestCase(
Expand All @@ -134,8 +166,13 @@ object ReportManager {
}
)

private fun processFullJunitResult(args: IArgs, matrices: MatrixMap, testShardChunks: ShardChunks) {
val testSuite = processXmlFromApi(matrices, args, withStackTraces = true)
private fun processFullJunitResult(
args: IArgs,
matrices: MatrixMap,
testShardChunks: ShardChunks,
testExecutions: List<TestExecution>
) {
val testSuite = testExecutions.createJUnitTestResult(withStackTraces = true)
FullJUnitReport.run(matrices, testSuite, printToStdout = false, args = args)
processJunitXml(testSuite, args, testShardChunks)
}
Expand Down
16 changes: 16 additions & 0 deletions test_runner/src/test/kotlin/ftl/gc/GcStorageTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package ftl.gc

import com.google.api.services.toolresults.model.AppStartTime
import com.google.api.services.toolresults.model.Duration
import com.google.api.services.toolresults.model.PerfMetricsSummary
import ftl.args.AndroidArgs
import ftl.test.util.FlankTestRunner
import io.mockk.every
Expand Down Expand Up @@ -27,6 +30,19 @@ class GcStorageTest {
GcStorage.upload(androidArgs.appApk!!, "does-not-exist", "nope")
}

@Test
fun `should upload performance metrics`() {
// given
val expectedPerformanceMetrics = PerfMetricsSummary()
.setAppStartTime(AppStartTime().setInitialDisplayTime(Duration().setSeconds(5)))

// when
GcStorage.uploadPerformanceMetrics(expectedPerformanceMetrics, "bucket", "path/test")

// then
assertTrue(GcStorage.exist("gs://bucket/path/test/performanceMetrics.json"))
}

@Test
fun `fix duplicated file names`() {
Assert.assertEquals(
Expand Down
16 changes: 16 additions & 0 deletions test_runner/src/test/kotlin/ftl/gc/GcToolResultsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ftl.gc
import com.google.api.client.googleapis.json.GoogleJsonResponseException
import com.google.api.client.http.HttpResponseException
import com.google.api.services.testing.model.ToolResultsHistory
import com.google.api.services.testing.model.ToolResultsStep
import com.google.common.truth.Truth.assertThat
import ftl.args.AndroidArgs
import ftl.config.FtlConstants
Expand Down Expand Up @@ -192,4 +193,19 @@ POST https://oauth2.googleapis.com/token
assertEquals(expected, exception.message)
}
}

@Test
fun `should properly get Performance Metrics`() {
// given
val toolsStepResults = ToolResultsStep().setStepId("1").setExecutionId("2").setHistoryId("3").setProjectId("4")

// when
val response = GcToolResults.getPerformanceMetric(toolsStepResults)

// then
assertThat(response.stepId).isEqualTo(toolsStepResults.stepId)
assertThat(response.executionId).isEqualTo(toolsStepResults.executionId)
assertThat(response.projectId).isEqualTo(toolsStepResults.projectId)
assertThat(response.historyId).isEqualTo(toolsStepResults.historyId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ftl.reports.api

import com.google.api.services.testing.model.AndroidDevice
import com.google.api.services.testing.model.TestExecution
import com.google.api.services.testing.model.ToolResultsStep
import com.google.common.truth.Truth.assertThat
import ftl.android.AndroidCatalog
import ftl.gc.GcStorage
import ftl.gc.GcToolResults
import ftl.test.util.FlankTestRunner
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(FlankTestRunner::class)
class PerformanceMetricsTest {

@After
fun tearDown() = unmockkAll()

@Test
fun `should not get and upload performance metrics for virtual devices`() {
mockkObject(AndroidCatalog) {
every { AndroidCatalog.isVirtualDevice(any<AndroidDevice>(), any()) } returns true

assertThat(testExecutions.map { it to "path" }.getAndUploadPerformanceMetrics("b8ce")).isEmpty()
}
}

@Test
fun `should get and upload performance metrics for physical devices`() {
val expectedBucket = "bucket"
val expectedPath = "path"

mockkObject(AndroidCatalog) {
every { AndroidCatalog.isVirtualDevice(any<AndroidDevice>(), any()) } returns false
val performanceMetrics = testExecutions.map {
GcToolResults.getPerformanceMetric(it.toolResultsStep)
}

mockkObject(GcStorage) {
testExecutions.map { it to expectedPath }.getAndUploadPerformanceMetrics(expectedBucket)
performanceMetrics.forEach {
verify { GcStorage.uploadPerformanceMetrics(it, expectedBucket, expectedPath) }
}
}
}
}

private val testExecutions = (1..5).map {
mockk<TestExecution> {
every { projectId } returns "test"
every { toolResultsStep } returns toolResultsStep(it)
every { environment } returns mockk {
every { androidDevice } returns AndroidDevice().setAndroidModelId(it.toString())
}
}
}

private fun toolResultsStep(id: Int) = ToolResultsStep()
.setStepId(id.toString())
.setExecutionId(id.toString())
.setHistoryId(id.toString())
.setProjectId(id.toString())
}
Loading

0 comments on commit 68d52d8

Please sign in to comment.