From a580db2f15d3cae09dc1b967abfbe1d35fe54fc3 Mon Sep 17 00:00:00 2001 From: OmniLab Team Date: Tue, 2 Jun 2026 09:34:04 -0700 Subject: [PATCH] Clean up obsolete client-side multi-device allocation diagnostics. PiperOrigin-RevId: 925402871 --- .../labinfo/proto/lab_info_service.proto | 21 + .../api/model/error/InfraErrorId.java | 2 +- .../diagnostic/AllocationDiagnostician.java | 39 -- .../allocation/diagnostic/Assessment.java | 30 -- .../allocation/diagnostic/multidevice/BUILD | 48 --- .../diagnostic/multidevice/LabAssessment.java | 214 ---------- .../diagnostic/multidevice/LabAssessor.java | 29 -- .../diagnostic/multidevice/LabReport.java | 313 -------------- .../multidevice/MultiDeviceDiagnostician.java | 111 ----- .../allocation/diagnostic/singledevice/BUILD | 52 --- .../singledevice/SingleDeviceAssessment.java | 349 --------------- .../singledevice/SingleDeviceAssessor.java | 48 --- .../SingleDeviceDiagnostician.java | 298 ------------- .../singledevice/SingleDeviceReport.java | 397 ------------------ .../infra/client/api/controller/job/BUILD | 8 +- .../client/api/controller/job/JobRunner.java | 131 +++--- .../infra/master/rpc/stub/LabInfoStub.java | 5 + .../master/rpc/stub/grpc/LabInfoGrpcStub.java | 14 + .../mobileharness/shared/labinfo/BUILD | 1 + .../shared/labinfo/LabInfoService.java | 13 + .../mobileharness/client/api/util/stub/BUILD | 3 + .../shared/size/BinarySizeTest.java | 4 +- 22 files changed, 135 insertions(+), 1995 deletions(-) delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/AllocationDiagnostician.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/Assessment.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/BUILD delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessment.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessor.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabReport.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/MultiDeviceDiagnostician.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/BUILD delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessment.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessor.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceDiagnostician.java delete mode 100644 src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceReport.java diff --git a/src/devtools/mobileharness/shared/labinfo/proto/lab_info_service.proto b/src/devtools/mobileharness/shared/labinfo/proto/lab_info_service.proto index 09918b7185..ef735599cc 100644 --- a/src/devtools/mobileharness/shared/labinfo/proto/lab_info_service.proto +++ b/src/devtools/mobileharness/shared/labinfo/proto/lab_info_service.proto @@ -26,6 +26,9 @@ option java_outer_classname = "LabInfoServiceProto"; service LabInfoService { // Gets lab and device information. rpc GetLabInfo(GetLabInfoRequest) returns (GetLabInfoResponse); + + // Diagnoses job allocation failure. + rpc DiagnoseJob(DiagnoseJobRequest) returns (DiagnoseJobResponse) {} } message GetLabInfoRequest { @@ -45,3 +48,21 @@ message GetLabInfoResponse { // Required. mobileharness.api.query.LabQueryResult lab_query_result = 1; } + +message DiagnoseJobRequest { + // Required. The ID of the job to diagnose. + string job_id = 1; +} + +message DiagnoseJobResponse { + // Required. The human-readable diagnostic report. + string readable_report = 1; + + enum ErrorType { + UNKNOWN = 0; + INFRA_ERROR = 1; + USER_CONFIG_ERROR = 2; + } + // Required. The error type of the allocation failure. + ErrorType error_type = 2; +} diff --git a/src/java/com/google/devtools/mobileharness/api/model/error/InfraErrorId.java b/src/java/com/google/devtools/mobileharness/api/model/error/InfraErrorId.java index d49cb6342f..5198d2e73f 100644 --- a/src/java/com/google/devtools/mobileharness/api/model/error/InfraErrorId.java +++ b/src/java/com/google/devtools/mobileharness/api/model/error/InfraErrorId.java @@ -229,7 +229,6 @@ public enum InfraErrorId implements ErrorId { MASTER_RPC_STUB_LAB_SYNC_REMOVE_MISSING_DEVICES_ERROR(49_291, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_LAB_INFO_GET_LAB_INFO_ERROR(49_260, ErrorType.INFRA_ISSUE), - MASTER_RPC_STUB_LAB_INFO_GET_LAB_METADATAS_ERROR(49_261, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_LAB_INFO_GET_LAB_OVERVIEWS_ERROR(49_262, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_LAB_INFO_GET_LAB_SUMMARIES_ERROR(49_263, ErrorType.INFRA_ISSUE), @@ -237,6 +236,7 @@ public enum InfraErrorId implements ErrorId { MASTER_RPC_STUB_LAB_INFO_GET_DEVICE_DETAIL_ERROR(49_265, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_LAB_INFO_GET_DEVICE_DIMENSIONS_ERROR(49_266, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_LAB_INFO_GET_COMPRESSED_DEVICE_METADATAS_ERROR(49_267, ErrorType.INFRA_ISSUE), + MASTER_RPC_STUB_LAB_INFO_DIAGNOSE_JOB_ERROR(49_268, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_JOB_INFO_GET_JOB_SUMMARY_LIST_ERROR(49_271, ErrorType.INFRA_ISSUE), MASTER_RPC_STUB_JOB_INFO_GET_JOB_DETAIL_ERROR(49_272, ErrorType.INFRA_ISSUE), diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/AllocationDiagnostician.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/AllocationDiagnostician.java deleted file mode 100644 index 945903d53e..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/AllocationDiagnostician.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic; - -import com.google.devtools.mobileharness.api.model.error.MobileHarnessException; -import java.util.Optional; - -/** AllocationDiagnostician provides a report for identifying the cause of an allocation failure. */ -public interface AllocationDiagnostician { - - /** - * Generates a {@link Report} on why the job failed to allocate devices. This may be invoked - * multiple times while the job is waiting for device allocation. The worst report generated - * during this time will be surfaced. - * - * @param noPerfectCandidate whether there's perfect candidates. - */ - Report diagnoseJob(boolean noPerfectCandidate) - throws MobileHarnessException, InterruptedException; - - /** Returns the report from the previous call to {@link #diagnoseJob()} if there was one. */ - Optional getLastReport(); - - void logExtraInfo(); -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/Assessment.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/Assessment.java deleted file mode 100644 index adefcfc72f..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/Assessment.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic; - -/** Assessment evaluates the ability of a particular resource to fulfill job requirements. */ -public interface Assessment { - - /** Adds a resource to be considered for scoring */ - Assessment addResource(T resource); - - /** - * Returns an integer reflecting how well a resource or group of resources satisfies the job - * requirements. - */ - int getScore(); -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/BUILD b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/BUILD deleted file mode 100644 index f7640a0b3e..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/BUILD +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2022 Google 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. -# - -# Description -# Allocation diagnostics for multi-device tests. - -load("@rules_java//java:defs.bzl", "java_library") - -package( - default_applicable_licenses = ["//:license"], - default_visibility = [ - "//java/com/google/devtools/mobileharness/infra/client/api/controller:__subpackages__", - "//javatests/com/google/devtools/mobileharness/infra/client/api/controller:__subpackages__", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/job:__subpackages__", - ], -) - -java_library( - name = "multidevice", - srcs = glob(["*.java"]), - deps = [ - "//src/devtools/mobileharness/api/query/proto:device_query_java_proto", - "//src/java/com/google/devtools/mobileharness/api/model/error", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/device:querier", - "//src/java/com/google/devtools/mobileharness/shared/util/auto:auto_value", - "//src/java/com/google/wireless/qa/mobileharness/shared/constant:dimension", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/job:job_schedule_unit", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/job/in", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/lab", - "@maven//:com_google_code_gson_gson", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - ], -) diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessment.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessment.java deleted file mode 100644 index fad7a916e6..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessment.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice; - -import static java.lang.Math.max; -import static java.lang.Math.min; - -import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ListMultimap; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Assessment; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice.SingleDeviceAssessment; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice.SingleDeviceAssessor; -import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier.LabQueryResult; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Name; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Value; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.model.job.in.SubDeviceSpec; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceInfo; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** An {@link Assessment} of a lab host and its ability to satisfy job requirements. */ -public class LabAssessment implements Assessment { - - private final JobScheduleUnit job; - private final SingleDeviceAssessor assessor; - private final Map requirementsToOverallAssessments; - private final ListMultimap requirementsToSortedCandidates; - private String hostname; - private Optional score; - - public LabAssessment(JobScheduleUnit job) { - this(job, new SingleDeviceAssessor()); - } - - @VisibleForTesting - LabAssessment(JobScheduleUnit job, SingleDeviceAssessor assessor) { - this.job = job; - this.assessor = assessor; - this.requirementsToOverallAssessments = new HashMap<>(); - this.requirementsToSortedCandidates = ArrayListMultimap.create(); - this.score = Optional.empty(); - } - - /** - * Adds a lab and its devices to this assessment for scoring consideration. Since MH only supports - * allocating devices from a single lab at a time, this should only be called once per Assessment. - * In other words, only one lab should be added. - */ - @CanIgnoreReturnValue - @Override - public LabAssessment addResource(LabQueryResult lab) { - for (SubDeviceSpec spec : job.subDeviceSpecs().getAllSubDevices()) { - requirementsToOverallAssessments.put(spec, assessor.assess(job, spec, lab.devices())); - for (DeviceInfo device : lab.devices()) { - if (!isPossibleCandidate(device)) { - continue; - } - String id = device.locator().getSerial(); - SingleDeviceAssessment assessment = assessor.assess(job, spec, device); - requirementsToSortedCandidates.put(spec, DeviceCandidate.create(id, assessment)); - } - requirementsToSortedCandidates - .get(spec) - .sort((c1, c2) -> c2.assessment().getScore() - c1.assessment().getScore()); - } - this.hostname = lab.hostname(); - return this; - } - - private boolean isPossibleCandidate(DeviceInfo deviceInfo) { - boolean deviceHasSimCardInfoDimension = - deviceInfo.dimensions().supported().get(Name.SIM_CARD_INFO).stream() - .anyMatch(value -> !value.equals(Value.NO_SIM)); - boolean jobHasSimCardInfoDimension = job.dimensions().get(Name.SIM_CARD_INFO) != null; - if (!jobHasSimCardInfoDimension && deviceHasSimCardInfoDimension) { - return false; - } - - boolean deviceHasNonDefaultPoolNameDimension = - deviceInfo.dimensions().supported().get(Name.POOL_NAME).stream() - .anyMatch(value -> !Value.DEFAULT_POOL_NAME.equals(value)); - boolean jobHasNonDefaultPoolNameDimension = - job.dimensions().get(Name.POOL_NAME) != null - && !Value.DEFAULT_POOL_NAME.equals(job.dimensions().get(Name.POOL_NAME)); - if (!jobHasNonDefaultPoolNameDimension && deviceHasNonDefaultPoolNameDimension) { - return false; - } - return true; - } - - /** - * @see {@link Assessment#getScore()}. - *

This value returned from this is cached and it is efficient to call getScore several - * times. - */ - @Override - public int getScore() { - if (score.isEmpty()) { - List specs = job.subDeviceSpecs().getAllSubDevices(); - score = Optional.of(computeScore(specs, 0, new HashSet<>(), new HashSet<>())); - } - return score.get(); - } - - /** - * Computes the score of this assessment. - * - * @param specs the device requirements - * @param specIndex the index of the requirement in specs that is currently being matched for - * scoring - * @param currentCandidateSet the current set of candidate devices already matched to requirements - * @param currentSerials the current set of device serials already matched to requirements - */ - private int computeScore( - List specs, - int specIndex, - Set currentCandidateSet, - Set currentSerials) { - if (currentCandidateSet.size() == specs.size()) { - int sum = 0; - for (DeviceCandidate candidate : currentCandidateSet) { - sum += candidate.assessment().getScore(); - } - return sum; - } - - List candidates = requirementsToSortedCandidates.get(specs.get(specIndex)); - int bestScore = 0; - int searchDepth = - min(2, candidates.size()); // Select at most 2 candidates to reduce running time. - for (int i = 0; i < searchDepth; i++) { - DeviceCandidate candidate = candidates.get(i); - if (currentSerials.contains(candidate.id())) { - continue; - } - currentCandidateSet.add(candidate); - currentSerials.add(candidate.id()); - bestScore = - max(bestScore, computeScore(specs, specIndex + 1, currentCandidateSet, currentSerials)); - currentCandidateSet.remove(candidate); - currentSerials.remove(candidate.id()); - } - - return bestScore; - } - - /** Returns whether this {@link Assessment} has the maximum possible score. */ - public boolean hasMaxScore() { - return getScore() - == (SingleDeviceAssessment.MAX_SCORE * job.subDeviceSpecs().getSubDeviceCount()); - } - - /** Returns the hostname of the lab resource under consideration. */ - String getHostname() { - return hostname; - } - - /** - * Returns the overall {@link SingleDeviceAssessment} for the given spec. This overall assessment - * determines whether there are any specific requirements (dimensions, decorators, availability, - * etc.) that cannot be satisfied with any devices at the lab host under evaluation. If the - * returned assessment has a maximum score, this does not imply the spec can be matched, but - * rather that each requirement in the spec is satisfied by some device. - */ - SingleDeviceAssessment getOverallDeviceAssessment(SubDeviceSpec spec) { - return requirementsToOverallAssessments.get(spec); - } - - /** - * Returns the top limit {@link DeviceCandidates} for the given spec. The returned list will be - * sorted in non-increasing order by {@link SingleDeviceAssessment#getScore()}. - */ - ImmutableList getTopCandidates(SubDeviceSpec spec, int limit) { - List candidates = requirementsToSortedCandidates.get(spec); - if (candidates.size() > limit) { - return ImmutableList.copyOf(candidates.subList(0, limit)); - } - return ImmutableList.copyOf(candidates); - } - - @AutoValue - abstract static class DeviceCandidate { - public abstract String id(); - - public abstract SingleDeviceAssessment assessment(); - - public static DeviceCandidate create(String id, SingleDeviceAssessment assessment) { - return new AutoValue_LabAssessment_DeviceCandidate(id, assessment); - } - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessor.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessor.java deleted file mode 100644 index a13598830f..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabAssessor.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice; - -import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier.LabQueryResult; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; - -/** Generates {@link LabAssessment}s for a set of requirements and lab details. */ -public class LabAssessor { - - /** Returns a {@link LabAssessment} for the given job and lab results. */ - public LabAssessment assess(JobScheduleUnit job, LabQueryResult labResult) { - return new LabAssessment(job).addResource(labResult); - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabReport.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabReport.java deleted file mode 100644 index 489c4021af..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/LabReport.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice; - -import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.devtools.mobileharness.api.model.error.InfraErrorId; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Report; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice.LabAssessment.DeviceCandidate; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice.SingleDeviceAssessment; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.model.job.in.SubDeviceSpec; -import java.time.Duration; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.PriorityQueue; -import java.util.stream.Collectors; - -/** A {@link Report} for a lab host and its ability to satisfy multi-device job requirements. */ -public final class LabReport implements Report { - - private static final String INDENT = " "; - private static final int MAX_DEVICES_PER_REQUIREMENT = 5; - static final int MAX_LABS = 15; - - private final JobScheduleUnit job; - private final PriorityQueue assessments; - private final Map hostToAssessments; - - public LabReport(JobScheduleUnit job) { - this.job = job; - this.assessments = new PriorityQueue<>((lab1, lab2) -> lab1.getScore() - lab2.getScore()); - this.hostToAssessments = new HashMap<>(); - } - - @Override - public boolean hasPerfectMatch() { - return assessments.stream().anyMatch(LabAssessment::hasMaxScore); - } - - /** - * Adds a {@link LabAssessment} to this Report to be included in the readable diagnosis. - * - *

If the it's the first round check, adds the {@link LabAssessment} to the priority queue. - * - *

If it's not the first round, Replace the existing {@link LabAssessment} for the same host if - * it has a higher score than the existing {@link LabAssessment}. - */ - @CanIgnoreReturnValue - LabReport addLabAssessment(LabAssessment labAssessment, boolean isFirstRound) { - String hostname = labAssessment.getHostname(); - if (isFirstRound) { - assessments.add(labAssessment); - hostToAssessments.put(hostname, labAssessment); - if (assessments.size() > MAX_LABS) { - LabAssessment removedLabAssessment = assessments.poll(); - hostToAssessments.remove(removedLabAssessment.getHostname()); - } - } else { - LabAssessment existingLabAssessment = hostToAssessments.get(hostname); - if (existingLabAssessment != null) { - if (labAssessment.getScore() < existingLabAssessment.getScore()) { - assessments.remove(existingLabAssessment); - assessments.add(labAssessment); - } - } - } - return this; - } - - @VisibleForTesting - ImmutableList getSortedAssessments() { - return ImmutableList.sortedCopyOf( - (lab1, lab2) -> lab2.getScore() - lab1.getScore(), assessments); - } - - /** - * @see {@link Report#getResult()} - */ - @Override - public Report.Result getResult() { - if (assessments.isEmpty()) { - return Report.Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - "There are no device supporting the user requested device type.", - null); - } - - ImmutableList labs = getSortedAssessments(); - StringBuilder readableReport = new StringBuilder(); - List specs = job.subDeviceSpecs().getAllSubDevices(); - - writeRequirements(readableReport, specs); - - // If there are labs that can satisfy the requirement something else is probably wrong - List perfectLabs = getPerfectLabs(labs); - if (!perfectLabs.isEmpty()) { - if (job.setting().getTimeout().getStartTimeoutMs() < Duration.ofSeconds(60).toMillis()) { - return Report.Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - String.format( - "OmniLab failed to allocate any devices within %d ms. " - + "Please increase your start_timeout setting " - + "to >60 seconds and try again.\n", - job.setting().getTimeout().getStartTimeoutMs()), - null); - } - readableReport.append( - "Your job should be able to allocate devices on any of the following lab hosts but" - + " OmniLab failed to allocate them. Please try again. "); - writeLabs(readableReport, specs, perfectLabs); - return Report.Result.create( - InfraErrorId.CLIENT_JR_ALLOC_INFRA_ERROR, readableReport.toString(), null); - } - - // List the top MAX_LABS candidates - readableReport.append( - String.format( - "No lab host was able to satisfy all requirements. These are the top %d closest" - + " matches.\n\n", - labs.size())); - writeLabs(readableReport, specs, labs); - return Report.Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, readableReport.toString(), null); - } - - private static void writeRequirements(StringBuilder readableReport, List specs) { - readableReport.append("Given the following device requirements:\n\n"); - for (int i = 0; i < specs.size(); i++) { - readableReport.append( - String.format( - "Requirement %d:\n%s\n\n", - i + 1, SubDeviceSpecFormatter.create(specs.get(i)).toJson())); - } - } - - private void writeLabs( - StringBuilder readableReport, List specs, List labs) { - for (LabAssessment lab : labs) { - readableReport.append( - String.format( - "%s Score %d %s\n\nHostname: %s\n\n", - Report.LINE_SEPARATOR, lab.getScore(), Report.LINE_SEPARATOR, lab.getHostname())); - for (int i = 0; i < specs.size(); i++) { - writeDevicesForRequirement(readableReport, specs.get(i), lab, i + 1); - } - } - } - - private void writeDevicesForRequirement( - StringBuilder readableReport, SubDeviceSpec spec, LabAssessment lab, int requirementNumber) { - SingleDeviceAssessment overallAssessment = lab.getOverallDeviceAssessment(spec); - if (!overallAssessment.hasMaxScore()) { - readableReport.append( - String.format( - "Requirement %d: No device can satisfy the following requirements:\n", - requirementNumber)); - writeDeviceErrors(readableReport, overallAssessment, INDENT); - readableReport.append("\n"); - return; - } - - ImmutableList candidates = - lab.getTopCandidates(spec, MAX_DEVICES_PER_REQUIREMENT); - if (candidates.get(0).assessment().hasMaxScore()) { - List perfectCandidates = - candidates.stream() - .filter(candidate -> candidate.assessment().hasMaxScore()) - .collect(Collectors.toList()); - readableReport.append( - String.format( - "Requirement %d can be fulfilled with the following devices:\n", requirementNumber)); - for (DeviceCandidate candidate : perfectCandidates) { - readableReport.append(String.format("%s- %s\n", INDENT, candidate.id())); - } - - List imperfectCandidates = - candidates.stream() - .filter(candidate -> !candidate.assessment().hasMaxScore()) - .collect(Collectors.toList()); - if (!imperfectCandidates.isEmpty()) { - readableReport.append( - String.format("Other candidates for requirement %d:\n", requirementNumber)); - for (DeviceCandidate candidate : imperfectCandidates) { - readableReport.append(String.format("%s- %s\n", INDENT, candidate.id())); - writeDeviceErrors(readableReport, candidate.assessment(), INDENT + INDENT); - } - } - - readableReport.append("\n"); - return; - } - - readableReport.append(String.format("Requirement %d top candidates:\n", requirementNumber)); - for (DeviceCandidate candidate : candidates) { - readableReport.append(String.format("%s- %s\n", INDENT, candidate.id())); - writeDeviceErrors(readableReport, candidate.assessment(), INDENT + INDENT); - } - readableReport.append("\n"); - } - - /** - * Writes error information for each {@link SingleDeviceAssessment}. The indent parameter allows - * device errors to be displayed hierarchically with respect to the device serial or the - * requirement number depending on the use case. - */ - private void writeDeviceErrors( - StringBuilder readableReport, SingleDeviceAssessment deviceAssessment, String indent) { - if (!deviceAssessment.isAccessible()) { - readableReport.append( - String.format( - "%s- %s (current user: %s)\n", indent, Report.NO_ACCESS, job.jobUser().getRunAs())); - } - if (!deviceAssessment.isDriverSupported()) { - readableReport.append(String.format("%s- %s\n", indent, Report.DRIVER_NOT_SUPPORTED)); - } - if (!deviceAssessment.isDeviceTypeSupported()) { - readableReport.append(String.format("%s- %s\n", indent, Report.DEVICE_TYPE_NOT_SUPPORTED)); - } - if (!deviceAssessment.isDecoratorsSupported()) { - readableReport.append( - String.format( - "%s- %s: %s\n", - indent, - Report.DECORATORS_NOT_SUPPORTED, - deviceAssessment.getUnsupportedDecorators().stream() - .sorted() - .collect(Collectors.toList()))); - } - if (!deviceAssessment.isDimensionsSupported()) { - readableReport.append( - String.format( - "%s- %s: %s\n", - indent, - Report.DIMENSIONS_NOT_SUPPORTED, - sortedMap(deviceAssessment.getUnsupportedDimensions()))); - } - if (!deviceAssessment.isDimensionsSatisfied()) { - Map unsatisfiedDimensions = new HashMap<>(); - for (Map.Entry entries : - deviceAssessment.getUnsatisfiedDimensions().entries()) { - unsatisfiedDimensions.put(entries.getKey(), entries.getValue()); - } - readableReport.append( - String.format( - "%s- %s: %s ", - indent, Report.DIMENSIONS_NOT_SATISFIED, sortedMap(unsatisfiedDimensions))); - } - if (deviceAssessment.isMissing()) { - readableReport.append(String.format("%s- %s\n", indent, Report.MISSING)); - } else if (!deviceAssessment.isIdle()) { - readableReport.append(String.format("%s- %s\n", indent, Report.NOT_IDLE)); - } - } - - private static List getPerfectLabs(List labs) { - return labs.stream().filter(LabAssessment::hasMaxScore).collect(Collectors.toList()); - } - - private static Map sortedMap(Map map) { - return map.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (oldValue, newValue) -> oldValue, - LinkedHashMap::new)); - } - - /** A utility for printing {@link SubDeviceSpec}s as JSON strings. */ - @AutoValue - abstract static class SubDeviceSpecFormatter { - abstract String type(); - - abstract ImmutableMap dimensions(); - - abstract ImmutableList decorators(); - - static SubDeviceSpecFormatter create(SubDeviceSpec spec) { - return new AutoValue_LabReport_SubDeviceSpecFormatter( - spec.type(), - ImmutableMap.copyOf(sortedMap(spec.dimensions().getAll())), - ImmutableList.sortedCopyOf(spec.decorators().getAll())); - } - - String toJson() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(this); - } - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/MultiDeviceDiagnostician.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/MultiDeviceDiagnostician.java deleted file mode 100644 index 90b0e975e5..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice/MultiDeviceDiagnostician.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice; - -import com.google.common.annotations.VisibleForTesting; -import com.google.devtools.mobileharness.api.model.error.MobileHarnessException; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.AllocationDiagnostician; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DeviceFilter; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DiagnosticDeviceQuerier; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Report; -import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier; -import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier.LabQueryResult; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceInfo; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery.DeviceQueryFilter; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery.DeviceQueryResult; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** An {@link AllocationDiagnostician} for multiple device jobs. */ -public final class MultiDeviceDiagnostician implements AllocationDiagnostician { - - private final LabAssessor assessor; - private final JobScheduleUnit job; - private final DiagnosticDeviceQuerier diagnosticQuerier; - - private volatile LabReport lastReport; - - public MultiDeviceDiagnostician(JobScheduleUnit job, DeviceQuerier deviceQuerier) { - this(new LabAssessor(), deviceQuerier, job); - } - - @VisibleForTesting - MultiDeviceDiagnostician(LabAssessor assessor, DeviceQuerier deviceQuerier, JobScheduleUnit job) { - this.assessor = assessor; - this.job = job; - this.diagnosticQuerier = new DiagnosticDeviceQuerier(deviceQuerier); - } - - /** - * @see {@link AllocationDiagnostician#getLastReport()} - */ - @Override - public Optional getLastReport() { - return Optional.ofNullable(lastReport); - } - - @Override - public void logExtraInfo() {} - - /** - * @see {@link AllocationDiagnostician#diagnoseJob()} - */ - @Override - public LabReport diagnoseJob(boolean noPerfectCandidate) - throws MobileHarnessException, InterruptedException { - DeviceQueryFilter filter = DeviceFilter.getFilter(job); - - DeviceQueryResult queryResult = diagnosticQuerier.queryDevice(job, filter); - - Map> labToDevices = new HashMap<>(); - for (DeviceQuery.DeviceInfo deviceProto : queryResult.getDeviceInfoList()) { - DeviceInfo deviceInfo = new DeviceInfo(deviceProto); - String hostname = deviceInfo.locator().getLabLocator().getHostName(); - labToDevices.computeIfAbsent(hostname, k -> new ArrayList<>()).add(deviceInfo); - } - - LabReport report; - boolean isFirstRound; - if (lastReport != null) { - report = lastReport; - isFirstRound = false; - } else { - report = new LabReport(job); - isFirstRound = true; - } - - for (Map.Entry> entry : labToDevices.entrySet()) { - String hostname = entry.getKey(); - List devices = entry.getValue(); - - if (devices.size() < job.subDeviceSpecs().getSubDeviceCount()) { - continue; - } - - LabQueryResult labResult = LabQueryResult.create(hostname, devices); - report.addLabAssessment(assessor.assess(job, labResult), isFirstRound); - } - - lastReport = report; - return report; - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/BUILD b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/BUILD deleted file mode 100644 index b2a67e8533..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/BUILD +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2022 Google 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. -# - -# Description: -# Allocation diagnostics for single device tests. - -load("@rules_java//java:defs.bzl", "java_library") - -package( - default_applicable_licenses = ["//:license"], - default_visibility = [ - "//src/java/com/google/devtools/mobileharness/infra/client:__subpackages__", - ], -) - -java_library( - name = "singledevice", - srcs = glob(["*.java"]), - deps = [ - "//src/devtools/mobileharness/api/model/proto:device_java_proto", - "//src/devtools/mobileharness/api/query/proto:device_query_java_proto", - "//src/java/com/google/devtools/mobileharness/api/model/error", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/device:querier", - "//src/java/com/google/devtools/mobileharness/shared/util/auto:auto_value", - "//src/java/com/google/devtools/mobileharness/shared/util/logging:google_logger", - "//src/java/com/google/devtools/mobileharness/shared/util/sharedpool:shared_pool_util", - "//src/java/com/google/wireless/qa/mobileharness/shared/constant:dimension", - "//src/java/com/google/wireless/qa/mobileharness/shared/constant:property", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/job", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/job:job_schedule_unit", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/job/in", - "//src/java/com/google/wireless/qa/mobileharness/shared/model/lab", - "//src/java/com/google/wireless/qa/mobileharness/shared/proto:job_java_proto", - "@maven//:com_google_code_findbugs_jsr305", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@protobuf//:protobuf_java", - ], -) diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessment.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessment.java deleted file mode 100644 index 89598d1bed..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessment.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice; - -import static com.google.common.collect.ImmutableMap.toImmutableMap; - -import com.google.common.base.Ascii; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import com.google.common.collect.SetMultimap; -import com.google.devtools.mobileharness.api.model.proto.Device.DeviceStatus; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Assessment; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.model.job.in.SubDeviceSpec; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceInfo; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Stream; -import javax.annotation.Nullable; - -/** Assessment of a device or a group of device, about their support of a given job */ -public class SingleDeviceAssessment implements Assessment { - private static final String DEVICE_DEFAULT_OWNER = "mobileharness-device-default-owner"; - - // RULES for the weight values: - // 1) To promote the devices when they support a certain requirement, increase the weight. - // But if it is not supported, the ranking will be extremely low. - // 2) To promote the devices when they don't support a certain requirement, decrease the weight. - // But even when it is support, it won't significantly increase the ranking too. - static final int WEIGHT_ACCESS = 3; - static final int WEIGHT_DEVICE_TYPE = 2; - static final int WEIGHT_DRIVER = 2; - static final int WEIGHT_DECORATOR = 5; - static final int WEIGHT_SUPPORTED_DIMENSION = 5; - static final int WEIGHT_SATISFIED_DIMENSION = 4; - static final int WEIGHT_STATUS = 1; - - public static final int MAX_SCORE = - WEIGHT_ACCESS - + WEIGHT_DEVICE_TYPE - + WEIGHT_DRIVER - + WEIGHT_DECORATOR - + WEIGHT_SUPPORTED_DIMENSION - + WEIGHT_SATISFIED_DIMENSION - + WEIGHT_STATUS; - static final int MIN_SCORE = 0; - - // Extra score added to those devices set to default owner. - static final int SUPPLEMENT_HAS_POTENTIAL_ACCESS = WEIGHT_ACCESS - 1; - - // The mismatch of a single dimension will drop 2 score. So that one dimension un-matched devices' - // scores are smaller than those busy or mobileharness-device-default-owner devices. - static final int DEDUCTION_SINGLE_DIMENSION = 2; - - // When user requires the "label", decrease the ranks of those that don't have "label" to - // promote the devices with "label". - static final int DEDUCTION_LABEL_DIMENSION = 3; - - // When user requires the "id" or "host_name" or "host_ip", decrease the ranks of those that don't - // support these fields. - static final int DEDUCTION_STRONG_DIMENSION = 4; - - private final String user; - private final String driver; - private final String deviceType; - private final ImmutableMap requestedDimensions; - private boolean accessible = false; - private boolean potentialAccessible = false; - private boolean driverSupported = false; - private boolean deviceTypeSupported = false; - private Set unsupportedDecorators; - private Map unsupportedDimensions; - private Multimap unsatisfiedDimensions; - - private boolean idle = false; - private boolean missing = true; - - private final List requestedSharedDimensionNames; - private final ListMultimap supportedSharedDimensions; - - /** Assessment of a job. */ - SingleDeviceAssessment(JobScheduleUnit job) { - this( - job, - null, - new HashSet<>(job.type().getDecoratorList()), - new HashMap<>(job.dimensions().getAll())); - } - - SingleDeviceAssessment(JobScheduleUnit job, SubDeviceSpec spec) { - this( - job, - spec, - new HashSet<>(spec.decorators().getAll()), - new HashMap<>(spec.dimensions().getAll())); - } - - private SingleDeviceAssessment( - JobScheduleUnit job, - @Nullable SubDeviceSpec spec, - Set unsupportedDecorators, - Map unsupportedDimensions) { - this.user = job.jobUser().getRunAs(); - this.driver = job.type().getDriver(); - this.requestedSharedDimensionNames = job.subDeviceSpecs().getSharedDimensionNames(); - this.deviceType = spec == null ? job.type().getDevice() : spec.type(); - this.requestedDimensions = - ImmutableMap.copyOf(spec == null ? job.dimensions().getAll() : spec.dimensions().getAll()); - this.unsupportedDecorators = unsupportedDecorators; - this.unsupportedDimensions = unsupportedDimensions; - this.unsatisfiedDimensions = null; - this.supportedSharedDimensions = LinkedListMultimap.create(); - } - - /** Adds a device into the assessment for a job. */ - @CanIgnoreReturnValue - @Override - public SingleDeviceAssessment addResource(DeviceInfo device) { - // A device can only be potentially accessible when it's not accessible. - if (device.owners().support(user) || device.executors().support(user)) { - accessible = true; - potentialAccessible = false; - } else if (!accessible) { - Set owners = device.owners().getAll(); - if (owners.size() == 1 && owners.contains(DEVICE_DEFAULT_OWNER)) { - potentialAccessible = true; - } - } - driverSupported |= device.drivers().support(driver); - deviceTypeSupported |= device.types().support(deviceType); - - if (!unsupportedDecorators.isEmpty()) { - unsupportedDecorators = device.decorators().getUnsupported(unsupportedDecorators); - } - if (!unsupportedDimensions.isEmpty()) { - unsupportedDimensions = - device - .dimensions() - .getUnsupportedJobDimensions(unsupportedDimensions, /* failFast= */ false); - } - for (String sharedDimensionName : requestedSharedDimensionNames) { - Stream.concat( - device.dimensions().supported().get(sharedDimensionName).stream(), - device.dimensions().required().get(sharedDimensionName).stream()) - .forEach( - dimensionValue -> supportedSharedDimensions.put(sharedDimensionName, dimensionValue)); - } - DeviceStatus deviceStatus = device.status().get(); - idle |= (deviceStatus == DeviceStatus.IDLE); - missing &= (deviceStatus == DeviceStatus.MISSING); - - // Check device required dimensions. - Multimap newUnsatisfiedDimensions = - device - .dimensions() - .getUnsatisfiedDeviceDimensions(requestedDimensions, /* failFast= */ false); - if (unsatisfiedDimensions == null) { - unsatisfiedDimensions = HashMultimap.create(); - unsatisfiedDimensions.putAll(newUnsatisfiedDimensions); - } else if (!unsatisfiedDimensions.isEmpty()) { - // Get intersection. - if (newUnsatisfiedDimensions.isEmpty()) { - unsatisfiedDimensions.clear(); - } else { - SetMultimap intersection = HashMultimap.create(); - unsatisfiedDimensions.forEach( - (key, value) -> { - if (newUnsatisfiedDimensions.containsEntry(key, value)) { - intersection.put(key, value); - } - }); - unsatisfiedDimensions = intersection; - } - } - return this; - } - - /** Whether any devices are accessible for users of the job. */ - public boolean isAccessible() { - return accessible; - } - - /** - * Whether any devices can become accessible if changing its owner from default value to the user - */ - public boolean isPotentialAccessible() { - return potentialAccessible; - } - - /** Whether any devices support the required driver of the job. */ - public boolean isDriverSupported() { - return driverSupported; - } - - /** Whether any devices support the required device type of the job. */ - public boolean isDeviceTypeSupported() { - return deviceTypeSupported; - } - - /** Whether any devices support the required decorators of the job. */ - public boolean isDecoratorsSupported() { - return unsupportedDecorators.isEmpty(); - } - - /** Gets the required decorators that are not supported by any devices. */ - public Set getUnsupportedDecorators() { - return Collections.unmodifiableSet(unsupportedDecorators); - } - - /** Whether any devices support the required dimensions of the job. */ - public boolean isDimensionsSupported() { - return unsupportedDimensions.isEmpty() - && supportedSharedDimensions.keySet().size() == requestedSharedDimensionNames.size(); - } - - /** Gets the job required dimensions which are not supported by any devices. */ - public Map getUnsupportedDimensions() { - if (supportedSharedDimensions.keySet().size() != requestedSharedDimensionNames.size()) { - ImmutableMap unsupportedSharedDimensions = - requestedSharedDimensionNames.stream() - .filter(name -> !supportedSharedDimensions.containsKey(name)) - .collect(toImmutableMap(Function.identity(), entry -> "")); - if (unsupportedDimensions.isEmpty()) { - return Collections.unmodifiableMap(unsupportedSharedDimensions); - } else { - return Collections.unmodifiableMap( - Stream.concat( - unsupportedDimensions.entrySet().stream(), - unsupportedSharedDimensions.entrySet().stream()) - .collect(toImmutableMap(Entry::getKey, Entry::getValue, (first, second) -> first))); - } - } else { - return Collections.unmodifiableMap(unsupportedDimensions); - } - } - - public Multimap getSupportedSharedDimensions() { - return supportedSharedDimensions; - } - - /** Whether any device required dimensions are already satisfied by the job dimensions. */ - public boolean isDimensionsSatisfied() { - return unsatisfiedDimensions.isEmpty(); - } - - /** Gets the device required dimensions which are not requested by the job. */ - public Multimap getUnsatisfiedDimensions() { - return Multimaps.unmodifiableMultimap(unsatisfiedDimensions); - } - - /** Whether any devices are idle. */ - public boolean isIdle() { - return idle; - } - - /** Whether all devices are missing. */ - public boolean isMissing() { - return missing; - } - - /** Return true if all the requirements are matched but the device is busy. */ - public boolean isRequirementMatchedButBusy() { - return getScore() == MAX_SCORE - (WEIGHT_STATUS - MIN_SCORE) && !isIdle(); - } - - /** - * Calculates a score of the overall support of the devices for the job. The devices with bigger - * score numbers will be promoted in the candidate device suggestion. - * - *

If the score is {@link #MAX_SCORE}, it means all job requirements are supported. - * - *

When no job requirement is supported, the score still could be larger than {@link - * #MIN_SCORE}. - */ - @Override - public int getScore() { - return getAccessibleScore() - + (driverSupported ? WEIGHT_DRIVER : MIN_SCORE) - + (deviceTypeSupported ? WEIGHT_DEVICE_TYPE : MIN_SCORE) - + Math.max(MIN_SCORE, WEIGHT_DECORATOR - unsupportedDecorators.size()) - + getSupportedDimensionScore() - + getSatisfiedDimensionScore() - + (idle ? WEIGHT_STATUS : MIN_SCORE); - } - - public boolean hasMaxScore() { - return getScore() == MAX_SCORE; - } - - private int getAccessibleScore() { - if (accessible) { - return WEIGHT_ACCESS; - } else if (potentialAccessible) { - return MIN_SCORE + SUPPLEMENT_HAS_POTENTIAL_ACCESS; - } else { - return MIN_SCORE; - } - } - - private int getSupportedDimensionScore() { - Map calculatedUnsupportedDimensions = getUnsupportedDimensions(); - int score = - WEIGHT_SUPPORTED_DIMENSION - - calculatedUnsupportedDimensions.size() * DEDUCTION_SINGLE_DIMENSION; - if (calculatedUnsupportedDimensions.containsKey( - Ascii.toLowerCase(Dimension.Name.LABEL.name()))) { - score = score - DEDUCTION_LABEL_DIMENSION + DEDUCTION_SINGLE_DIMENSION; - } - if (calculatedUnsupportedDimensions.containsKey(Ascii.toLowerCase(Dimension.Name.ID.name())) - || calculatedUnsupportedDimensions.containsKey( - Ascii.toLowerCase(Dimension.Name.HOST_IP.name())) - || calculatedUnsupportedDimensions.containsKey( - Ascii.toLowerCase(Dimension.Name.HOST_NAME.name()))) { - score = score - DEDUCTION_STRONG_DIMENSION + DEDUCTION_SINGLE_DIMENSION; - } - return Math.max(MIN_SCORE, score); - } - - private int getSatisfiedDimensionScore() { - int score = WEIGHT_SATISFIED_DIMENSION - unsatisfiedDimensions.size(); - return Math.max(MIN_SCORE, score); - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessor.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessor.java deleted file mode 100644 index ac761ffe74..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceAssessor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice; - -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.model.job.in.SubDeviceSpec; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceInfo; -import java.util.List; - -/** Assessor for providing detail assessment of the support of a group of devices for a job. */ -public class SingleDeviceAssessor { - /** Assesses the support of the given job with the given device. */ - public SingleDeviceAssessment assess(JobScheduleUnit job, DeviceInfo device) { - return new SingleDeviceAssessment(job).addResource(device); - } - - /** Assesses the support of the given job with the given device. */ - public SingleDeviceAssessment assess(JobScheduleUnit job, List devices) { - SingleDeviceAssessment assessment = new SingleDeviceAssessment(job); - devices.forEach(assessment::addResource); - return assessment; - } - - public SingleDeviceAssessment assess(JobScheduleUnit job, SubDeviceSpec spec, DeviceInfo device) { - return new SingleDeviceAssessment(job, spec).addResource(device); - } - - public SingleDeviceAssessment assess( - JobScheduleUnit job, SubDeviceSpec spec, List devices) { - SingleDeviceAssessment assessment = new SingleDeviceAssessment(job, spec); - devices.forEach(assessment::addResource); - return assessment; - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceDiagnostician.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceDiagnostician.java deleted file mode 100644 index 415afa87c6..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceDiagnostician.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static java.util.stream.Collectors.joining; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.FluentLogger; -import com.google.devtools.mobileharness.api.model.error.MobileHarnessException; -import com.google.devtools.mobileharness.api.model.proto.Device.DeviceStatus; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.AllocationDiagnostician; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DeviceFilter; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DiagnosticDeviceQuerier; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Report; -import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier; -import com.google.devtools.mobileharness.shared.util.sharedpool.SharedPoolJobUtil; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Name; -import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Value; -import com.google.wireless.qa.mobileharness.shared.constant.PropertyName.Job; -import com.google.wireless.qa.mobileharness.shared.model.job.JobInfo; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceInfo; -import com.google.wireless.qa.mobileharness.shared.model.lab.DeviceLocator; -import com.google.wireless.qa.mobileharness.shared.model.lab.LabLocator; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery.DeviceQueryFilter; -import com.google.wireless.qa.mobileharness.shared.proto.query.DeviceQuery.DimensionFilter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.annotation.Nullable; - -/** For diagnostic the reasons about why a job can not allocate devices. */ -public class SingleDeviceDiagnostician implements AllocationDiagnostician { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - /** - * If the perfect device count is less than this value, we'll only query the perfect devices - * instead of all the devices. - */ - private static final int MAX_QUERY_DEVICE_COUNT = 20; - - /** The job to diagnostic with. */ - private final JobInfo job; - - /** For retrieving the device information for diagnostic. */ - private final DeviceQuerier querier; - - /** Assessor for providing detail assessment of the support of a group of devices for a job. */ - private final SingleDeviceAssessor assessor; - - private final DiagnosticDeviceQuerier diagnosticQuerier; - - /** The report of the previous diagnostic. */ - @Nullable private volatile SingleDeviceReport lastReport; - - private final List historyReports = new ArrayList<>(); - - /** Creates a diagnostician for checking the reason why a job can not allocate devices. */ - public SingleDeviceDiagnostician(JobInfo job, DeviceQuerier querier) { - this(job, querier, new SingleDeviceAssessor()); - } - - @VisibleForTesting - SingleDeviceDiagnostician(JobInfo job, DeviceQuerier querier, SingleDeviceAssessor assessor) { - this.job = job; - this.querier = querier; - this.assessor = assessor; - this.diagnosticQuerier = new DiagnosticDeviceQuerier(querier); - } - - /** Gets the last report if the last invocation of {@link #diagnoseJob()}. */ - @Override - public Optional getLastReport() { - return Optional.ofNullable(lastReport); - } - - /** - * Diagnostic the job that fails to allocate any devices. - * - *

This method can be invoked multiple times when the job is waiting for device allocation. And - * the report will reflect the worse case during the waiting of the allocation. - */ - @Override - public SingleDeviceReport diagnoseJob(boolean noPerfectCandidate) - throws MobileHarnessException, InterruptedException { - List candidates = queryDevices(); - SingleDeviceReport report; - - // If the report has been generated in the previous diagnose, use the previous report as - // the base report and only change the individual device assessment if necessary; - // Else, generate a new report; - if (lastReport != null) { - report = lastReport; - } else { - if (candidates.isEmpty()) { - report = new SingleDeviceReport(job, null, noPerfectCandidate); - } else { - report = new SingleDeviceReport(job, assessor.assess(job, candidates), noPerfectCandidate); - } - } - - boolean isUsingSharedPool = SharedPoolJobUtil.isUsingSharedPool(job); - boolean isProdMaster = isUsingProdMaster(job); - - // When the overall assessment has MAX_SCORE, means every job requirements can always be - // supported by some devices. But no single device supports all job requirements. So need to - // further assess each individual devices. - if (report.getOverallScore() == SingleDeviceAssessment.MAX_SCORE) { - boolean hasDeviceMatchRequirementButBusy = false; - for (DeviceInfo candidate : candidates) { - String deviceId = candidate.locator().toString(); - SingleDeviceAssessment deviceAssessment = assessor.assess(job, candidate); - - Optional preDeviceAssessment = report.getDeviceAssessment(deviceId); - // Use the device assessment of this time if - // 1. The device has no assessment before. - // 2. The previous device assessment has MAX_SCORE but the assessment this time has - // smaller score. - if (preDeviceAssessment.isEmpty() - || (preDeviceAssessment.get().getScore() == SingleDeviceAssessment.MAX_SCORE - && deviceAssessment.getScore() < SingleDeviceAssessment.MAX_SCORE)) { - report.setDeviceAssessment(deviceId, deviceAssessment); - } - if (deviceAssessment.isRequirementMatchedButBusy()) { - hasDeviceMatchRequirementButBusy = true; - } - } - } - - lastReport = report; - historyReports.add(report); - return report; - } - - private List queryDevices() throws MobileHarnessException, InterruptedException { - DeviceQueryFilter candidateFilter = getDeviceQueryFilter(); - List candidates = - diagnosticQuerier.queryDevice(job, candidateFilter).getDeviceInfoList().stream() - .map(this::convertDeviceInfo) - .collect(Collectors.toList()); - candidates = - candidates.stream() - .filter( - deviceInfo -> { - boolean deviceHasSimCardInfoDimension = - deviceInfo.dimensions().supported().get(Name.SIM_CARD_INFO).stream() - .anyMatch(value -> !value.equals(Value.NO_SIM)); - boolean jobHasSimCardInfoDimension = - job.dimensions().get(Name.SIM_CARD_INFO) != null; - if (!jobHasSimCardInfoDimension && deviceHasSimCardInfoDimension) { - return false; - } - - boolean deviceHasNonDefaultPoolNameDimension = - deviceInfo.dimensions().supported().get(Name.POOL_NAME).stream() - .anyMatch(value -> !Value.DEFAULT_POOL_NAME.equals(value)); - boolean jobHasNonDefaultPoolNameDimension = - job.dimensions().get(Name.POOL_NAME) != null - && !Value.DEFAULT_POOL_NAME.equals(job.dimensions().get(Name.POOL_NAME)); - if (!jobHasNonDefaultPoolNameDimension && deviceHasNonDefaultPoolNameDimension) { - return false; - } - return true; - }) - .collect(Collectors.toList()); - return candidates; - } - - /** - * Gets the device query filter.
- * If the report has been generated in the previous diagnose and there are not too many perfect - * candidates, only query the information of these perfect candidates; Else, query all the - * devices. - */ - private DeviceQueryFilter getDeviceQueryFilter() { - if (lastReport != null) { - ImmutableList devices = - lastReport.getPerfectMatchDevices().stream() - .map( - deviceUniversalId -> { - int splitterIndex = deviceUniversalId.indexOf('@'); - if (splitterIndex > 0) { - return deviceUniversalId.substring(0, splitterIndex); - } else { - return deviceUniversalId; - } - }) - .collect(toImmutableList()); - if (devices.size() < MAX_QUERY_DEVICE_COUNT) { - String value = String.join("|", devices); - DeviceQueryFilter.Builder filter = DeviceQueryFilter.newBuilder(); - filter.addDimensionFilter(DimensionFilter.newBuilder().setName("id").setValueRegex(value)); - return filter.build(); - } - } - - return DeviceFilter.getFilter(job); - } - - @Override - public void logExtraInfo() { - List perfectCandidates = new ArrayList<>(); - for (int i = 0; i < historyReports.size(); i++) { - Collection perfectCandidatesInCurrentReport = - historyReports.get(i).getPerfectMatchDevices(); - perfectCandidates.addAll(perfectCandidatesInCurrentReport); - logger.atInfo().log( - "Diagnose %d's perfect candidates: %s", - i, perfectCandidatesInCurrentReport.stream().collect(joining(", "))); - } - - for (String perfectCandidate : perfectCandidates) { - StringBuilder candidateLog = new StringBuilder(); - candidateLog.append("Score for ").append(perfectCandidate).append(": "); - for (SingleDeviceReport historyReport : historyReports) { - Optional deviceAssessment = - historyReport.getDeviceAssessment(perfectCandidate); - if (deviceAssessment.isPresent()) { - candidateLog.append(deviceAssessment.get().getScore()).append(" "); - } else { - candidateLog.append("N/A "); - } - } - logger.atInfo().log("%s", candidateLog); - } - } - - /** Converts the DeviceInfo proto returned by device query API to Java version. */ - private DeviceInfo convertDeviceInfo(DeviceQuery.DeviceInfo deviceProto) { - String labIp = - deviceProto.getDimensionList().stream() - .filter( - dimension -> dimension.getName().equalsIgnoreCase(Dimension.Name.HOST_IP.name())) - .map(DeviceQuery.Dimension::getValue) - .findFirst() - .orElse("unknown"); - String hostName = - deviceProto.getDimensionList().stream() - .filter( - dimension -> dimension.getName().equalsIgnoreCase(Dimension.Name.HOST_NAME.name())) - .map(DeviceQuery.Dimension::getValue) - .findFirst() - .orElse("unknown"); - LabLocator labLocator = new LabLocator(labIp, hostName); - DeviceLocator deviceLocator = new DeviceLocator(deviceProto.getId(), labLocator); - DeviceInfo deviceInfo = - new DeviceInfo( - deviceLocator, DeviceStatus.valueOf(Ascii.toUpperCase(deviceProto.getStatus()))); - deviceInfo.owners().addAll(deviceProto.getOwnerList()); - deviceInfo.executors().addAll(deviceProto.getExecutorList()); - deviceInfo.types().addAll(deviceProto.getTypeList()); - deviceInfo.drivers().addAll(deviceProto.getDriverList()); - deviceInfo.decorators().addAll(deviceProto.getDecoratorList()); - for (DeviceQuery.Dimension dimension : deviceProto.getDimensionList()) { - if (dimension.getRequired()) { - deviceInfo.dimensions().required().add(dimension.getName(), dimension.getValue()); - } else { - deviceInfo.dimensions().supported().add(dimension.getName(), dimension.getValue()); - } - } - return deviceInfo; - } - - private static boolean isUsingMaster(JobInfo job) { - return job.properties().getOptional(Job.MASTER_SPEC).orElse("").contains("master"); - } - - private static boolean isUsingProdMaster(JobInfo job) { - return isUsingMaster(job) - && job.properties() - .getOptional(Job.MASTER_SPEC) - .orElse("") - .toLowerCase(Locale.ROOT) - .contains("prod"); - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceReport.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceReport.java deleted file mode 100644 index 2913a83744..0000000000 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice/SingleDeviceReport.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Copyright 2022 Google 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 com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice; - -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.devtools.mobileharness.api.model.error.InfraErrorId; -import com.google.devtools.mobileharness.api.model.error.MobileHarnessException; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DeviceFilter; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Report; -import com.google.wireless.qa.mobileharness.shared.model.job.JobScheduleUnit; -import com.google.wireless.qa.mobileharness.shared.proto.Job.JobType; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; -import javax.annotation.concurrent.NotThreadSafe; - -/** - * Allocation diagnostic report for a job when it fails to allocate any device. - * - *

Never try to use this report when your job can actually allocate devices. Otherwise, the - * report message will be misleading. - */ -@NotThreadSafe -public class SingleDeviceReport implements Report { - - private final JobScheduleUnit job; - @Nullable private final SingleDeviceAssessment overallAssessment; - private final Map deviceIdsToAssessments = new HashMap<>(); - private final ListMultimap scoresToDeviceIds = ArrayListMultimap.create(); - private final boolean noPerfectCandidate; - private final int maxCandidateType; - - /** - * Create an allocation report for a job when it fails to allocate any device. - * - * @param overallAssessment Overall assessment of the job with all the device candidates, for - * quickly identify the job requirements which can't be supported by any devices. If overall - * assessment is null, means no candidate found after applying {@link DeviceFilter}. - */ - SingleDeviceReport( - JobScheduleUnit job, - @Nullable SingleDeviceAssessment overallAssessment, - boolean noPerfectCandidate) { - this.job = Preconditions.checkNotNull(job); - this.overallAssessment = overallAssessment; - this.noPerfectCandidate = noPerfectCandidate; - this.maxCandidateType = 30; - } - - /** Returns the score of the overall assessment. */ - int getOverallScore() { - return overallAssessment == null - ? SingleDeviceAssessment.MIN_SCORE - : overallAssessment.getScore(); - } - - /** - * Returns the overall assessment. For quickly identify the job requirements which can't be - * supported by any devices. If overall assessment is absent, means no device candidate found. - */ - Optional getOverallAssessment() { - return Optional.ofNullable(overallAssessment); - } - - /** - * Saves/Updates the assessment of a single device for the current job. Individual device is only - * assessed when the overall assessment of the job is lower than {@link - * SingleDeviceAssessment#MAX_SCORE}. - */ - void setDeviceAssessment(String deviceId, SingleDeviceAssessment deviceAssessment) { - SingleDeviceAssessment preDeviceAssessment = - deviceIdsToAssessments.put(deviceId, deviceAssessment); - if (preDeviceAssessment != null) { - scoresToDeviceIds.remove(preDeviceAssessment.getScore(), deviceId); - } - scoresToDeviceIds.put(deviceAssessment.getScore(), deviceId); - } - - /** - * Retrieves the assessment of a single device for the current job. Individual device is only - * assessed when the overall assessment of the job is lower than {@link - * SingleDeviceAssessment#MAX_SCORE}. - */ - Optional getDeviceAssessment(String deviceId) { - return Optional.ofNullable(deviceIdsToAssessments.get(deviceId)); - } - - /** Returns whether there's at least one device that can match all user requirement. */ - @Override - public boolean hasPerfectMatch() { - return scoresToDeviceIds.containsKey(SingleDeviceAssessment.MAX_SCORE); - } - - Collection getPerfectMatchDevices() { - return scoresToDeviceIds.get(SingleDeviceAssessment.MAX_SCORE); - } - - /** Generates the result with the human readable report, as well as the allocation error type. */ - @Override - public Result getResult() { - // For shared pool skip diagnostic. - JobType jobType = job.type(); - - if (overallAssessment == null) { - if (jobType.getDevice().equals("AndroidLocalEmulator")) { - return Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - "No " + jobType.getDevice() + " found" + getDiagnosticCandidateFilterSuffix(), - null); - } - return Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - "No " + jobType.getDevice() + " found" + getDiagnosticCandidateFilterSuffix(), - null); - } - - StringBuilder report = new StringBuilder(); - - MobileHarnessException cause = null; - if (overallAssessment.getScore() < SingleDeviceAssessment.MAX_SCORE) { - if (!overallAssessment.isAccessible()) { - String msg = - String.format( - "You (%s) don't have access to any %s%s. Please use the 'run_as' flag to specify" - + " an authorized MDB group you are in, or contact the device owners" - + " to request access.\n", - job.jobUser().getRunAs(), - jobType.getDevice(), - getDiagnosticCandidateFilterSuffix()); - report.append(msg); - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_NO_ACCESS, msg); - } - if (!overallAssessment.isDriverSupported()) { - report.append( - String.format( - "No %s can support driver %s%s.\n", - jobType.getDevice(), jobType.getDriver(), getDiagnosticCandidateFilterSuffix())); - } - if (!overallAssessment.isDeviceTypeSupported()) { - report.append( - String.format( - "No %s can support device type %s%s.\n", - jobType.getDevice(), jobType.getDevice(), getDiagnosticCandidateFilterSuffix())); - } - if (!overallAssessment.isDecoratorsSupported()) { - report.append( - String.format( - "No %s can support decorators %s%s.\n", - jobType.getDevice(), - overallAssessment.getUnsupportedDecorators(), - getDiagnosticCandidateFilterSuffix())); - } - if (!overallAssessment.isDimensionsSupported()) { - String msg = - String.format( - "No %s can support job dimensions %s%s.\n", - jobType.getDevice(), - overallAssessment.getUnsupportedDimensions(), - getDiagnosticCandidateFilterSuffix()); - report.append(msg); - if (cause == null) { // Do not override the cause - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_NOT_EXIST, msg); - } - } - if (!overallAssessment.isDimensionsSatisfied()) { - report.append( - String.format( - "Job does not satisfy the required dimensions" + " of any %s %s%s.\n", - jobType.getDevice(), - overallAssessment.getUnsatisfiedDimensions(), - getDiagnosticCandidateFilterSuffix())); - } - if (!overallAssessment.isIdle()) { - report.append( - String.format( - "No IDLE %s%s. Please extend the timeout settings to wait longer.\n", - jobType.getDevice(), getDiagnosticCandidateFilterSuffix())); - } - - return Result.create( - InfraErrorId.CLIENT_JR_MNM_ALLOC_DEVICE_NOT_SATISFY_SLO, report.toString(), cause); - } - - // Checks whether there are any devices can support all requirements. - Collection goodIds = scoresToDeviceIds.get(SingleDeviceAssessment.MAX_SCORE); - if (!goodIds.isEmpty()) { - if (job.setting().getTimeout().getStartTimeoutMs() < Duration.ofSeconds(60).toMillis()) { - return Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - report - .append( - String.format( - "OmniLab failed to allocate any devices within %d ms. " - + "Please increase your start_timeout setting " - + "to >60 seconds and try again", - job.setting().getTimeout().getStartTimeoutMs())) - .toString(), - null); - } else if (noPerfectCandidate) { - return Result.create( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, - report - .append( - String.format( - "OmniLab failed to find suitable device with allocation exit strategy %s." - + " Consider to use another allocation exit strategy.", - job.setting().getAllocationExitStrategy())) - .toString(), - null); - } else { - report - .append("Your job should be able to allocate the following ") - .append(goodIds.size()) - .append(" devices but OmniLab somehow failed to allocate them. Please try again. ") - .append(Joiner.on("\n - ").join(goodIds.stream().limit(maxCandidateType).iterator())); - if (goodIds.size() > maxCandidateType) { - report - .append("\n - ...(truncated ") - .append(goodIds.size() - maxCandidateType) - .append(" devices)..."); - } - return Result.create(InfraErrorId.CLIENT_JR_ALLOC_INFRA_ERROR, report.toString(), null); - } - } - - // Gives suggestions. - // And also generates a cause with the best guess. - List candidateTypes = new ArrayList<>(maxCandidateType); - for (int score = SingleDeviceAssessment.MAX_SCORE - 1; - score >= SingleDeviceAssessment.MIN_SCORE; - score--) { - Collection ids = scoresToDeviceIds.get(score); - if (ids.isEmpty()) { - continue; - } - // Groups the devices with the same error together as one candidate type. - ListMultimap errorToIds = LinkedListMultimap.create(); - for (String id : ids) { - SingleDeviceAssessment assessment = deviceIdsToAssessments.get(id); - if (assessment != null) { - StringBuilder error = new StringBuilder(); - error.append("============ Score ").append(score).append(" ============\nErrors:"); - if (!assessment.isAccessible()) { - error.append( - String.format( - "\n - %s (current user: %s)", Report.NO_ACCESS, job.jobUser().getRunAs())); - if (cause == null) { - String msg = - String.format( - "%s (current user: %s) for device %s.", - Report.NO_ACCESS, job.jobUser().getRunAs(), id); - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_NO_ACCESS, msg); - } - } - if (assessment.isPotentialAccessible()) { - error - .append( - "\n - POTENTIAL_ACCESS: The device owner is the default value. Need to change" - + " to the current user: ") - .append(job.jobUser().getRunAs()); - if (cause == null) { - String msg = - String.format( - "POTENTIAL_ACCESS: The device %s owner is the default value. Need to change " - + " to the current user: %s", - id, job.jobUser().getRunAs()); - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_NO_ACCESS, msg); - } - } - if (!assessment.isDriverSupported()) { - error.append( - String.format("\n - %s: %s", Report.DRIVER_NOT_SUPPORTED, jobType.getDriver())); - } - if (!assessment.isDeviceTypeSupported()) { - error.append( - String.format( - "\n - %s: %s", Report.DEVICE_TYPE_NOT_SUPPORTED, jobType.getDevice())); - } - if (!assessment.isDecoratorsSupported()) { - error.append( - String.format( - "\n - %s: %s", - Report.DECORATORS_NOT_SUPPORTED, assessment.getUnsupportedDecorators())); - } - if (!assessment.isDimensionsSupported()) { - error.append( - String.format( - "\n - %s: %s", - Report.DIMENSIONS_NOT_SUPPORTED, assessment.getUnsupportedDimensions())); - } - if (!assessment.isDimensionsSatisfied()) { - error.append( - String.format( - "\n - %s: %s", - Report.DIMENSIONS_NOT_SATISFIED, assessment.getUnsatisfiedDimensions())); - } - if (assessment.isMissing()) { - error.append(String.format("\n - %s", Report.MISSING)); - if (cause == null) { - String msg = String.format("%s for device %s.", Report.MISSING, id); - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_MISSING, msg); - } - } else if (!assessment.isIdle()) { - error.append(String.format("\n - %s", Report.NOT_IDLE)); - if (cause == null) { - String msg = String.format("%s for device %s.", Report.NOT_IDLE, id); - cause = - new MobileHarnessException( - InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR_DEVICE_BUSY, msg); - } - } - errorToIds.put(error.toString(), id); - } - } - - // Collects all the candidate types of the same score. - for (String error : errorToIds.keySet()) { - List idsWithSameError = errorToIds.get(error); - StringBuilder candidateType = new StringBuilder(); - candidateType - .append(error) - .append("\nCandidates:\n - ") - .append(Joiner.on("\n - ").join(idsWithSameError.stream().limit(2).iterator())); - if (idsWithSameError.size() > 2) { - candidateType - .append("\n - (truncated ") - .append(idsWithSameError.size() - 2) - .append(" devices)"); - } - candidateTypes.add(candidateType.toString()); - if (candidateTypes.size() >= maxCandidateType) { - break; - } - } - if (candidateTypes.size() >= maxCandidateType) { - break; - } - } - - // No candidate found. - if (candidateTypes.isEmpty()) { - return Result.create( - InfraErrorId.CLIENT_JR_ALLOC_INFRA_ERROR, - "Diagnostician can not determine why devices were not allocated.", - null); - } - - // Print the candidate types. - report - .append( - "No device can meet all of your requirements." - + " Did you mean to use one of the following devices:\n") - .append(Joiner.on("\n").join(candidateTypes)); - if (candidateTypes.size() >= maxCandidateType) { - report.append("\n==== (truncated other candidate devices) ===="); - } - return Result.create(InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR, report.toString(), cause); - } - - /** A postfix about diagnostic candidate filter in the report. */ - private String getDiagnosticCandidateFilterSuffix() { - return ""; - } -} diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/BUILD b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/BUILD index 067ae336b7..4815c798f8 100644 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/BUILD +++ b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/BUILD @@ -30,12 +30,16 @@ java_library( ], deps = [ ":pending_test_printer", + "//java/com/google/devtools/mobileharness/infra/master/rpc/stub:flag", + "//java/com/google/net/rpc/contrib/parambuilder:combinedserverspec", "//src/devtools/common/metrics/stability/model/proto:error_java_proto", "//src/devtools/mobileharness/api/messaging:messaging_java_proto", "//src/devtools/mobileharness/api/model/proto:job_java_proto", "//src/devtools/mobileharness/api/model/proto:test_java_proto", "//src/devtools/mobileharness/api/query/proto:device_query_java_proto", + "//src/devtools/mobileharness/shared/labinfo/proto:lab_info_service_java_proto", "//src/java/com/google/devtools/common/metrics/stability/converter", + "//src/java/com/google/devtools/common/metrics/stability/rpc:exception", "//src/java/com/google/devtools/mobileharness/api/messaging:exception", "//src/java/com/google/devtools/mobileharness/api/model/allocation", "//src/java/com/google/devtools/mobileharness/api/model/error", @@ -45,8 +49,6 @@ java_library( "//src/java/com/google/devtools/mobileharness/api/model/lab:locator", "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/allocator", "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/multidevice", - "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/allocation/diagnostic/singledevice", "//src/java/com/google/devtools/mobileharness/infra/client/api/controller/device:querier", "//src/java/com/google/devtools/mobileharness/infra/client/api/mode", "//src/java/com/google/devtools/mobileharness/infra/client/api/mode/remote:exception", @@ -62,6 +64,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/infra/controller/test/manager", "//src/java/com/google/devtools/mobileharness/infra/controller/test/manager:direct_test_runner_util", "//src/java/com/google/devtools/mobileharness/infra/controller/test/util:exception_handler", + "//src/java/com/google/devtools/mobileharness/infra/master/rpc/stub:lab_info", "//src/java/com/google/devtools/mobileharness/shared/constant:log_record_importance", "//src/java/com/google/devtools/mobileharness/shared/constant/closeable:mobile_harness_auto_closeable", "//src/java/com/google/devtools/mobileharness/shared/file/resolver:local_file_resolver", @@ -82,6 +85,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/shared/util/time:time_utils", "//src/java/com/google/wireless/qa/mobileharness/client/api/event:job", "//src/java/com/google/wireless/qa/mobileharness/client/api/event/internal", + "//src/java/com/google/wireless/qa/mobileharness/client/api/util/stub", "//src/java/com/google/wireless/qa/mobileharness/shared/api/validator:job_checker", "//src/java/com/google/wireless/qa/mobileharness/shared/constant:dimension", "//src/java/com/google/wireless/qa/mobileharness/shared/constant:property", diff --git a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/JobRunner.java b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/JobRunner.java index 0f1385ca99..9211063802 100644 --- a/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/JobRunner.java +++ b/src/java/com/google/devtools/mobileharness/infra/client/api/controller/job/JobRunner.java @@ -44,6 +44,7 @@ import com.google.devtools.common.metrics.stability.converter.ErrorModelConverter; import com.google.devtools.common.metrics.stability.model.proto.ErrorTypeProto.ErrorType; import com.google.devtools.common.metrics.stability.model.proto.ExceptionProto.ExceptionDetail; +import com.google.devtools.common.metrics.stability.rpc.RpcExceptionWithErrorId; import com.google.devtools.mobileharness.api.messaging.proto.MessagingProto.MessageSend; import com.google.devtools.mobileharness.api.model.allocation.Allocation; import com.google.devtools.mobileharness.api.model.error.ErrorId; @@ -57,12 +58,9 @@ import com.google.devtools.mobileharness.api.model.proto.Test.TestResult; import com.google.devtools.mobileharness.infra.client.api.controller.allocation.allocator.AllocationWithStats; import com.google.devtools.mobileharness.infra.client.api.controller.allocation.allocator.DeviceAllocator; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.AllocationDiagnostician; import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DeviceFilter; import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.DeviceFilter.FilterType; import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.Report; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.multidevice.MultiDeviceDiagnostician; -import com.google.devtools.mobileharness.infra.client.api.controller.allocation.diagnostic.singledevice.SingleDeviceDiagnostician; import com.google.devtools.mobileharness.infra.client.api.controller.device.DeviceQuerier; import com.google.devtools.mobileharness.infra.client.api.mode.ExecMode; import com.google.devtools.mobileharness.infra.client.api.mode.remote.JobCancelledException; @@ -75,7 +73,11 @@ import com.google.devtools.mobileharness.infra.controller.test.manager.DirectTestRunnerUtil; import com.google.devtools.mobileharness.infra.controller.test.manager.TestManager; import com.google.devtools.mobileharness.infra.controller.test.util.SubscriberExceptionLoggingHandler; +import com.google.devtools.mobileharness.infra.master.rpc.stub.LabInfoStub; +import com.google.devtools.mobileharness.infra.master.rpc.stub.MasterStubFlag; import com.google.devtools.mobileharness.shared.constant.closeable.MobileHarnessAutoCloseable; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobRequest; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobResponse; import com.google.devtools.mobileharness.shared.util.algorithm.GraphMatching; import com.google.devtools.mobileharness.shared.util.comm.messaging.poster.TestMessagePoster; import com.google.devtools.mobileharness.shared.util.concurrent.Callables; @@ -86,9 +88,11 @@ import com.google.devtools.mobileharness.shared.util.time.Sleeper; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.inject.AbstractModule; +import com.google.net.rpc.contrib.parambuilder.CombinedServerSpec; import com.google.wireless.qa.mobileharness.client.api.event.JobEndEvent; import com.google.wireless.qa.mobileharness.client.api.event.JobStartEvent; import com.google.wireless.qa.mobileharness.client.api.event.internal.JobFirstAllocationEvent; +import com.google.wireless.qa.mobileharness.client.api.util.stub.StubManager; import com.google.wireless.qa.mobileharness.shared.api.validator.JobChecker; import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Name; import com.google.wireless.qa.mobileharness.shared.constant.Dimension.Value; @@ -228,9 +232,6 @@ public enum EventScope { private final PendingTestPrinter pendingTestPrinter; @Nullable private final SuitableDeviceChecker suitableDeviceChecker; - /** Util for diagnostic the reasons when the job fails to allocate devices. */ - private volatile AllocationDiagnostician allocDiagnostician; - private int diagnosticTimes = 0; /** The test message subscribers of the job. */ @@ -1325,9 +1326,7 @@ void finalizeJobResult( MobileHarnessException cause = null; if (diagnosticReport.isPresent()) { errorId = diagnosticReport.get().getResult().errorId(); - if (errorId == InfraErrorId.CLIENT_JR_ALLOC_INFRA_ERROR) { - allocDiagnostician.logExtraInfo(); - } + String diagnosticResult = diagnosticReport.get().getResult().readableReport(); if (errorId == InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR) { hasAllocFailTests = true; @@ -1572,75 +1571,83 @@ private Instant getNextPollAllocationTime(int countPollAllocation) { @CanIgnoreReturnValue private Optional diagnose(boolean noPerfectCandidate) throws InterruptedException { diagnosticTimes++; - if (deviceQuerier == null) { - logger.atInfo().log("DeviceQuerier is disabled, skip diagnose."); + + String execMode = jobInfo.properties().get(PropertyName.Job.EXEC_MODE); + if (!"remote".equalsIgnoreCase(execMode)) { + logger.atInfo().log( + "Job is not in remote mode (exec_mode=%s), skip master-side diagnose.", execMode); return Optional.empty(); } try { - if (allocDiagnostician == null) { - allocDiagnostician = createAllocationDiagnostician(jobInfo, deviceQuerier); - } - if (allocDiagnostician != null) { - // Double check to guarantee result accurate if previous check finds max score device. - // b/64825449 - if (allocDiagnostician.getLastReport().isEmpty() - || (allocDiagnostician.getLastReport().get().hasPerfectMatch() - && diagnosticTimes < MAX_ALLOCATION_DIAGNOSE_TIMES)) { - jobInfo - .log() - .atInfo() - .alsoTo(logger) - .log("Diagnose allocation failure of job %s...", jobInfo.locator().getId()); - // TODO: after the long-term solution is launched, always generate - // diagnostic. - if (Runtime.getRuntime().maxMemory() - <= Flags.lowerLimitOfJvmMaxMemoryAllowForAllocationDiagnostic.getNonNull()) { - String message = - String.format( - "Current max memory is set as %d, less than %d. To avoid OOM when querying all" - + " devices, we stop the diagnose.", - Runtime.getRuntime().maxMemory(), - Flags.lowerLimitOfJvmMaxMemoryAllowForAllocationDiagnostic.getNonNull()); - jobInfo - .warnings() - .addAndLog( - createExceptionWithoutStackTrace( - InfraErrorId.CLIENT_JR_ALLOC_DIAGNOSTIC_ERROR, message), - logger); - return Optional.empty(); - } else { - allocDiagnostician.diagnoseJob(noPerfectCandidate); - jobInfo - .log() - .atInfo() - .alsoTo(logger) - .log( - "Successfully generated allocation diagnostic report for job %s", - jobInfo.locator().getId()); - } - } - return allocDiagnostician.getLastReport(); - } - } catch (MobileHarnessException e) { + jobInfo + .log() + .atInfo() + .alsoTo(logger) + .log("Diagnose allocation failure of job %s via Master...", jobInfo.locator().getId()); + + CombinedServerSpec masterSpec = MasterStubFlag.getPrimaryMasterSpec(); + LabInfoStub stub = getLabInfoStub(masterSpec); + + DiagnoseJobRequest request = + DiagnoseJobRequest.newBuilder().setJobId(jobInfo.locator().getId()).build(); + + DiagnoseJobResponse response = stub.diagnoseJob(request); + + jobInfo + .log() + .atInfo() + .alsoTo(logger) + .log( + "Successfully generated allocation diagnostic report for job %s via Master", + jobInfo.locator().getId()); + + return Optional.of(new MasterDiagnosticReport(response)); + } catch (RpcExceptionWithErrorId e) { jobInfo .warnings() .addAndLog( createExceptionWithoutStackTrace( InfraErrorId.CLIENT_JR_ALLOC_DIAGNOSTIC_ERROR, - "Failed to diagnose the allocation failure", + "Failed to diagnose the allocation failure via Master", e), logger); } return Optional.empty(); } + private static class MasterDiagnosticReport implements Report { + private final DiagnoseJobResponse response; + + MasterDiagnosticReport(DiagnoseJobResponse response) { + this.response = response; + } + + @Override + public boolean hasPerfectMatch() { + return response.getErrorType() == DiagnoseJobResponse.ErrorType.INFRA_ERROR; + } + + @Override + public Result getResult() { + ErrorId errorId; + switch (response.getErrorType()) { + case INFRA_ERROR: + errorId = InfraErrorId.CLIENT_JR_ALLOC_INFRA_ERROR; + break; + case USER_CONFIG_ERROR: + errorId = InfraErrorId.CLIENT_JR_ALLOC_USER_CONFIG_ERROR; + break; + default: + errorId = InfraErrorId.CLIENT_JR_ALLOC_UNKNOWN_ERROR; + } + return Result.create(errorId, response.getReadableReport(), /* cause= */ null); + } + } + @VisibleForTesting - AllocationDiagnostician createAllocationDiagnostician( - JobInfo jobInfo, DeviceQuerier deviceQuerier) { - return jobInfo.subDeviceSpecs().hasMultipleDevices() - ? new MultiDeviceDiagnostician(jobInfo, deviceQuerier) - : new SingleDeviceDiagnostician(jobInfo, deviceQuerier); + LabInfoStub getLabInfoStub(CombinedServerSpec masterSpec) { + return StubManager.getInstance().getMasterNewLabInfoStub(masterSpec); } /** diff --git a/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/LabInfoStub.java b/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/LabInfoStub.java index 3976f0cae7..9afd957c58 100644 --- a/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/LabInfoStub.java +++ b/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/LabInfoStub.java @@ -18,6 +18,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.devtools.common.metrics.stability.rpc.RpcExceptionWithErrorId; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobRequest; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobResponse; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoRequest; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoResponse; @@ -29,4 +31,7 @@ public interface LabInfoStub { /** Gets lab info from Master asynchronously. */ ListenableFuture getLabInfoAsync(GetLabInfoRequest request); + + /** Diagnoses job allocation failure. */ + DiagnoseJobResponse diagnoseJob(DiagnoseJobRequest request) throws RpcExceptionWithErrorId; } diff --git a/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/grpc/LabInfoGrpcStub.java b/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/grpc/LabInfoGrpcStub.java index 30bbad79aa..fca9ca2ca8 100644 --- a/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/grpc/LabInfoGrpcStub.java +++ b/src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/grpc/LabInfoGrpcStub.java @@ -24,6 +24,8 @@ import com.google.devtools.mobileharness.api.model.error.InfraErrorId; import com.google.devtools.mobileharness.infra.master.rpc.stub.LabInfoStub; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceGrpc; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobRequest; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobResponse; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoRequest; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoResponse; import com.google.devtools.mobileharness.shared.util.comm.stub.GrpcDirectTargetConfigures; @@ -38,6 +40,8 @@ public class LabInfoGrpcStub implements LabInfoStub { /** Blocking interface for {@link LabInfoGrpcStub}. */ public interface BlockingInterface { GetLabInfoResponse getLabInfo(GetLabInfoRequest request); + + DiagnoseJobResponse diagnoseJob(DiagnoseJobRequest request); } /** Future interface for {@link LabInfoGrpcStub}. */ @@ -93,4 +97,14 @@ public ListenableFuture getLabInfoAsync(GetLabInfoRequest re InfraErrorId.MASTER_RPC_STUB_LAB_INFO_GET_LAB_INFO_ERROR, String.format("Failed to get lab info, request=%s", shortDebugString(request))); } + + @Override + public DiagnoseJobResponse diagnoseJob(DiagnoseJobRequest request) + throws GrpcExceptionWithErrorId { + return GrpcStubUtil.invoke( + blockingInterface::diagnoseJob, + request, + InfraErrorId.MASTER_RPC_STUB_LAB_INFO_DIAGNOSE_JOB_ERROR, + String.format("Failed to diagnose job, request=%s", shortDebugString(request))); + } } diff --git a/src/java/com/google/devtools/mobileharness/shared/labinfo/BUILD b/src/java/com/google/devtools/mobileharness/shared/labinfo/BUILD index 93c41a63f5..081d7b9582 100644 --- a/src/java/com/google/devtools/mobileharness/shared/labinfo/BUILD +++ b/src/java/com/google/devtools/mobileharness/shared/labinfo/BUILD @@ -59,6 +59,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/shared/util/auto:auto_value", "//src/java/com/google/devtools/mobileharness/shared/util/base:proto_text_format", "//src/java/com/google/devtools/mobileharness/shared/util/logging:google_logger", + "@grpc-java//core", "@grpc-java//stub", "@maven//:com_google_guava_guava", "@maven//:javax_inject_jsr330_api", diff --git a/src/java/com/google/devtools/mobileharness/shared/labinfo/LabInfoService.java b/src/java/com/google/devtools/mobileharness/shared/labinfo/LabInfoService.java index e2ec98dcfc..f22cdf884b 100644 --- a/src/java/com/google/devtools/mobileharness/shared/labinfo/LabInfoService.java +++ b/src/java/com/google/devtools/mobileharness/shared/labinfo/LabInfoService.java @@ -32,8 +32,11 @@ import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.LabQueryResult; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.Page; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceGrpc; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobRequest; +import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.DiagnoseJobResponse; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoRequest; import com.google.devtools.mobileharness.shared.labinfo.proto.LabInfoServiceProto.GetLabInfoResponse; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import java.time.Duration; import java.util.Optional; @@ -79,6 +82,16 @@ public void getLabInfo( LabInfoServiceGrpc.getGetLabInfoMethod()); } + // TODO: DiagnoseJob is handled by the Master server only. + @Override + public void diagnoseJob( + DiagnoseJobRequest request, StreamObserver responseObserver) { + responseObserver.onError( + Status.UNIMPLEMENTED + .withDescription("DiagnoseJob is only supported on the Master server") + .asRuntimeException()); + } + @VisibleForTesting GetLabInfoResponse doGetLabInfo(GetLabInfoRequest request) throws MobileHarnessException { String clientId = getClientId(); diff --git a/src/java/com/google/wireless/qa/mobileharness/client/api/util/stub/BUILD b/src/java/com/google/wireless/qa/mobileharness/client/api/util/stub/BUILD index cb49b30fba..48ba805321 100644 --- a/src/java/com/google/wireless/qa/mobileharness/client/api/util/stub/BUILD +++ b/src/java/com/google/wireless/qa/mobileharness/client/api/util/stub/BUILD @@ -35,6 +35,8 @@ java_library( "//src/java/com/google/devtools/mobileharness/infra/client/api/util/stub:stub_utils", "//src/java/com/google/devtools/mobileharness/infra/lab/rpc/stub:exec_test", "//src/java/com/google/devtools/mobileharness/infra/lab/rpc/stub:prepare_test", + "//src/java/com/google/devtools/mobileharness/infra/master/rpc/stub:lab_info", + "//src/java/com/google/devtools/mobileharness/infra/master/rpc/stub/grpc:lab_info", "//src/java/com/google/devtools/mobileharness/shared/constant/environment", "//src/java/com/google/devtools/mobileharness/shared/util/base:proto_text_format", "//src/java/com/google/devtools/mobileharness/shared/util/comm/filetransfer/common/factory:file_transfer_client_factories", @@ -42,6 +44,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/shared/util/flags", "//src/java/com/google/devtools/mobileharness/shared/version/rpc/stub", "//src/java/com/google/wireless/qa/mobileharness/shared/model/lab", + "@grpc-java//core", "@maven//:com_google_code_findbugs_jsr305", "@maven//:com_google_guava_guava", "@maven//:com_google_inject_guice", diff --git a/src/javatests/com/google/devtools/mobileharness/shared/size/BinarySizeTest.java b/src/javatests/com/google/devtools/mobileharness/shared/size/BinarySizeTest.java index a346eb2f54..b7d369bf03 100644 --- a/src/javatests/com/google/devtools/mobileharness/shared/size/BinarySizeTest.java +++ b/src/javatests/com/google/devtools/mobileharness/shared/size/BinarySizeTest.java @@ -59,9 +59,9 @@ public class BinarySizeTest { "ats_olc_server", 40_050_000L, "ats_olc_server_local_mode", - 43_950_000L, + 44_050_000L, "lab_server", - 45_350_000L, + 45_450_000L, "ats_console", 22_550_000L, "persistent_cache_manager",