Save autobisection results

Save autobisection results to the database using newly created rpc
service

Bug: 513167043
Change-Id: I6970c4313fec26e3f123fb28e9dd4c213226056e
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/1236278
Commit-Queue: Marcin Mordecki <mordeckimarcin@google.com>
Reviewed-by: Maxim Sheshukov <maximsheshukov@google.com>
diff --git a/perf/go/anomalygroup/service/service.go b/perf/go/anomalygroup/service/service.go
index 8341183..d2eb3c2 100644
--- a/perf/go/anomalygroup/service/service.go
+++ b/perf/go/anomalygroup/service/service.go
@@ -263,6 +263,7 @@
 			ImprovementDirection: paramset["improvement_direction"][0],
 			MedianBefore:         anomaly.MedianBefore,
 			MedianAfter:          anomaly.MedianAfter,
+			Id:                   anomaly.Id,
 		})
 	}
 
diff --git a/perf/go/anomalygroup/utils/anomalygrouputils.go b/perf/go/anomalygroup/utils/anomalygrouputils.go
index 583287a..1cc198d 100644
--- a/perf/go/anomalygroup/utils/anomalygrouputils.go
+++ b/perf/go/anomalygroup/utils/anomalygrouputils.go
@@ -138,11 +138,12 @@
 		//   service host url override. We should rename and use one single property.
 		wf, err := temporalClient.ExecuteWorkflow(
 			ctx, wo, workflows.MaybeTriggerBisection, &workflows.MaybeTriggerBisectionParam{
-				AnomalyGroupServiceUrl: config.Config.BackendServiceHostUrl,
-				CulpritServiceUrl:      config.Config.BackendServiceHostUrl,
-				AnomalyGroupId:         newGroupID.AnomalyGroupId,
-				GroupingTaskQueue:      config.Config.TemporalConfig.GroupingTaskQueue,
-				PinpointTaskQueue:      config.Config.TemporalConfig.PinpointTaskQueue,
+				AnomalyGroupServiceUrl:  config.Config.BackendServiceHostUrl,
+				AutobisectionServiceUrl: config.Config.BackendServiceHostUrl,
+				CulpritServiceUrl:       config.Config.BackendServiceHostUrl,
+				AnomalyGroupId:          newGroupID.AnomalyGroupId,
+				GroupingTaskQueue:       config.Config.TemporalConfig.GroupingTaskQueue,
+				PinpointTaskQueue:       config.Config.TemporalConfig.PinpointTaskQueue,
 			})
 		if err != nil {
 			return "", status.Errorf(codes.Internal, "Unable to start grouping workflow (%v).", err)
diff --git a/perf/go/backend/BUILD.bazel b/perf/go/backend/BUILD.bazel
index cd121a4..7e21f40 100644
--- a/perf/go/backend/BUILD.bazel
+++ b/perf/go/backend/BUILD.bazel
@@ -19,6 +19,8 @@
         "//perf/go/alerts",
         "//perf/go/anomalygroup:store",
         "//perf/go/anomalygroup/service",
+        "//perf/go/autobisection",
+        "//perf/go/autobisection/service",
         "//perf/go/backend/shared",
         "//perf/go/builders",
         "//perf/go/config",
@@ -50,6 +52,7 @@
         "//perf/go/alerts",
         "//perf/go/alerts/sqlalertstore",
         "//perf/go/anomalygroup/sqlanomalygroupstore",
+        "//perf/go/autobisection/sqlautobisectionstore",
         "//perf/go/config",
         "//perf/go/culprit/notify",
         "//perf/go/culprit/sqlculpritstore",
diff --git a/perf/go/backend/backend.go b/perf/go/backend/backend.go
index 4465853..fdbe37c 100644
--- a/perf/go/backend/backend.go
+++ b/perf/go/backend/backend.go
@@ -14,6 +14,8 @@
 	"go.skia.org/infra/perf/go/alerts"
 	"go.skia.org/infra/perf/go/anomalygroup"
 	ag_service "go.skia.org/infra/perf/go/anomalygroup/service"
+	"go.skia.org/infra/perf/go/autobisection"
+	autobisection_service "go.skia.org/infra/perf/go/autobisection/service"
 	"go.skia.org/infra/perf/go/backend/shared"
 	"go.skia.org/infra/perf/go/builders"
 	"go.skia.org/infra/perf/go/config"
@@ -57,6 +59,7 @@
 // initialize initializes the Backend application.
 func (b *Backend) initialize(
 	anomalygroupStore anomalygroup.Store,
+	autobisectionStore autobisection.Store,
 	culpritStore culprit.Store,
 	subscriptionStore subscription.Store,
 	regressionStore regression.Store,
@@ -132,6 +135,15 @@
 		}
 	}
 
+	sklog.Debug("Creating autobisection store.")
+	if autobisectionStore == nil {
+		autobisectionStore, err = builders.NewAutobisectionStoreFromConfig(ctx, config.Config)
+		if err != nil {
+			sklog.Errorf("Error creating autobisection store. %s", err)
+			return err
+		}
+	}
+
 	var temporalClient client.Client
 	if config.Config.NotifyConfig.Notifications == notifytypes.AnomalyGrouper {
 		// Temporal client needs to setup when grouping is in use.
@@ -154,6 +166,7 @@
 	services := []BackendService{
 		NewPinpointService(nil, nil),
 		ag_service.New(anomalygroupStore, culpritStore, regressionStore, temporalClient),
+		autobisection_service.New(autobisectionStore),
 		culprit_service.New(anomalygroupStore, culpritStore, subscriptionStore, notifier, config.Config),
 	}
 	err = b.configureServices(services)
@@ -240,6 +253,7 @@
 // New creates a new instance of Backend application.
 func New(flags *config.BackendFlags,
 	anomalygroupStore anomalygroup.Store,
+	autobisectionStore autobisection.Store,
 	culpritStore culprit.Store,
 	subscriptionStore subscription.Store,
 	regressionStore regression.Store,
@@ -253,7 +267,7 @@
 		flags:            flags,
 	}
 
-	err := b.initialize(anomalygroupStore, culpritStore, subscriptionStore, regressionStore, notifier)
+	err := b.initialize(anomalygroupStore, autobisectionStore, culpritStore, subscriptionStore, regressionStore, notifier)
 	return b, err
 }
 
diff --git a/perf/go/backend/backend_test.go b/perf/go/backend/backend_test.go
index c9737e0..8f0140a 100644
--- a/perf/go/backend/backend_test.go
+++ b/perf/go/backend/backend_test.go
@@ -11,6 +11,7 @@
 	"go.skia.org/infra/perf/go/alerts"
 	alert_store "go.skia.org/infra/perf/go/alerts/sqlalertstore"
 	ag_store "go.skia.org/infra/perf/go/anomalygroup/sqlanomalygroupstore"
+	"go.skia.org/infra/perf/go/autobisection/sqlautobisectionstore"
 	"go.skia.org/infra/perf/go/config"
 	"go.skia.org/infra/perf/go/culprit/notify"
 	culprit_store "go.skia.org/infra/perf/go/culprit/sqlculpritstore"
@@ -25,6 +26,7 @@
 	db := sqltest.NewSpannerDBForTests(t, "backend")
 	alertStore, _ := alert_store.New(db)
 	configProvider, _ := alerts.NewConfigProvider(context.Background(), alertStore, 600)
+	autobisectionStore, _ := sqlautobisectionstore.New(db)
 	anomalygroupStore, _ := ag_store.New(db)
 	culpritStore, _ := culprit_store.New(db)
 	subscriptionStore, _ := subscription_store.New(db)
@@ -39,7 +41,7 @@
 		PromPort:       ":0",
 		ConfigFilename: configFile,
 	}
-	b, err := New(flags, anomalygroupStore, culpritStore, subscriptionStore, regressionStore, &notify.DefaultCulpritNotifier{})
+	b, err := New(flags, anomalygroupStore, autobisectionStore, culpritStore, subscriptionStore, regressionStore, &notify.DefaultCulpritNotifier{})
 	require.NoError(t, err)
 	ch := make(chan interface{})
 	go func() {
diff --git a/perf/go/backend/backendserver/main.go b/perf/go/backend/backendserver/main.go
index e13806a..5f913ba 100644
--- a/perf/go/backend/backendserver/main.go
+++ b/perf/go/backend/backendserver/main.go
@@ -33,7 +33,7 @@
 				Flags:       (&backendFlags).AsCliFlags(),
 				Action: func(c *cli.Context) error {
 					urfavecli.LogFlags(c)
-					b, err := backend.New(&backendFlags, nil, nil, nil, nil, nil)
+					b, err := backend.New(&backendFlags, nil, nil, nil, nil, nil, nil)
 					if err != nil {
 						return err
 					}
diff --git a/perf/go/backend/client/BUILD.bazel b/perf/go/backend/client/BUILD.bazel
index d6a4ea7..8614624 100644
--- a/perf/go/backend/client/BUILD.bazel
+++ b/perf/go/backend/client/BUILD.bazel
@@ -10,6 +10,7 @@
         "//go/skerr",
         "//go/sklog",
         "//perf/go/anomalygroup/proto/v1",
+        "//perf/go/autobisection/proto/v1:proto",
         "//perf/go/config",
         "//perf/go/culprit/proto/v1",
         "//pinpoint/proto/v1:proto",
diff --git a/perf/go/backend/client/backendclientutil.go b/perf/go/backend/client/backendclientutil.go
index c8ee325..5d7cd41 100644
--- a/perf/go/backend/client/backendclientutil.go
+++ b/perf/go/backend/client/backendclientutil.go
@@ -8,6 +8,7 @@
 	"go.skia.org/infra/go/skerr"
 	"go.skia.org/infra/go/sklog"
 	anomalygroup "go.skia.org/infra/perf/go/anomalygroup/proto/v1"
+	autobisection "go.skia.org/infra/perf/go/autobisection/proto/v1"
 	"go.skia.org/infra/perf/go/config"
 	culprit "go.skia.org/infra/perf/go/culprit/proto/v1"
 	pinpoint "go.skia.org/infra/pinpoint/proto/v1"
@@ -105,6 +106,20 @@
 	return anomalygroup.NewAnomalyGroupServiceClient(conn), nil
 }
 
+// NewAutobisectionServiceClient returns a new instance of a client for the autobisection service.
+func NewAutobisectionServiceClient(backendServiceUrlOverride string, insecure_conn bool) (autobisection.AutobisectionServiceClient, error) {
+	if !isBackendEnabled(backendServiceUrlOverride) {
+		return nil, skerr.Fmt("Backend service is not enabled for this instance.")
+	}
+
+	conn, err := getGrpcConnection(backendServiceUrlOverride, insecure_conn)
+	if err != nil {
+		return nil, err
+	}
+
+	return autobisection.NewAutobisectionServiceClient(conn), nil
+}
+
 // NewCulpritServiceClient returns a new instance of a client for the culprit service.
 func NewCulpritServiceClient(backendServiceUrlOverride string, insecure_conn bool) (culprit.CulpritServiceClient, error) {
 	if !isBackendEnabled(backendServiceUrlOverride) {
diff --git a/perf/go/builders/BUILD.bazel b/perf/go/builders/BUILD.bazel
index e0f0001..4df1427 100644
--- a/perf/go/builders/BUILD.bazel
+++ b/perf/go/builders/BUILD.bazel
@@ -20,6 +20,8 @@
         "//perf/go/alerts/sqlalertstore",
         "//perf/go/anomalygroup:store",
         "//perf/go/anomalygroup/sqlanomalygroupstore",
+        "//perf/go/autobisection",
+        "//perf/go/autobisection/sqlautobisectionstore",
         "//perf/go/config",
         "//perf/go/culprit:store",
         "//perf/go/culprit/sqlculpritstore",
diff --git a/perf/go/builders/builders.go b/perf/go/builders/builders.go
index bf0eb6b..9d47197 100644
--- a/perf/go/builders/builders.go
+++ b/perf/go/builders/builders.go
@@ -23,6 +23,8 @@
 	"go.skia.org/infra/perf/go/alerts/sqlalertstore"
 	"go.skia.org/infra/perf/go/anomalygroup"
 	ag_store "go.skia.org/infra/perf/go/anomalygroup/sqlanomalygroupstore"
+	"go.skia.org/infra/perf/go/autobisection"
+	"go.skia.org/infra/perf/go/autobisection/sqlautobisectionstore"
 	"go.skia.org/infra/perf/go/config"
 	"go.skia.org/infra/perf/go/culprit"
 	culprit_store "go.skia.org/infra/perf/go/culprit/sqlculpritstore"
@@ -329,6 +331,16 @@
 	return ag_store.New(db)
 }
 
+// NewAutobisectionStoreFromConfig creates a new autobisection.Store from the
+// InstanceConfig which provides access to the autobisection data.
+func NewAutobisectionStoreFromConfig(ctx context.Context, instanceConfig *config.InstanceConfig) (autobisection.Store, error) {
+	db, err := getDBPool(ctx, instanceConfig)
+	if err != nil {
+		return nil, err
+	}
+	return sqlautobisectionstore.New(db)
+}
+
 // NewCulpritStoreFromConfig creates a new culprit.Store from the
 // InstanceConfig which provides access to the culprit data.
 func NewCulpritStoreFromConfig(ctx context.Context, instanceConfig *config.InstanceConfig) (culprit.Store, error) {
diff --git a/perf/go/workflows/internal/BUILD.bazel b/perf/go/workflows/internal/BUILD.bazel
index 77d60a6..3c375d4 100644
--- a/perf/go/workflows/internal/BUILD.bazel
+++ b/perf/go/workflows/internal/BUILD.bazel
@@ -5,6 +5,7 @@
     name = "internal",
     srcs = [
         "anomalygroup_service_activity.go",
+        "autobisection_service_activity.go",
         "culprit_service_activity.go",
         "gerrit_service_activity.go",
         "maybe_trigger_bisection.go",
@@ -18,6 +19,7 @@
         "//go/skerr",
         "//go/sklog",
         "//perf/go/anomalygroup/proto/v1",
+        "//perf/go/autobisection/proto/v1:proto",
         "//perf/go/backend/client",
         "//perf/go/culprit/proto/v1",
         "//perf/go/types",
@@ -41,6 +43,8 @@
     deps = [
         "//perf/go/anomalygroup/proto/v1",
         "//perf/go/anomalygroup/proto/v1/mocks",
+        "//perf/go/autobisection/proto/v1:proto",
+        "//perf/go/autobisection/proto/v1/mocks",
         "//perf/go/culprit/proto/v1",
         "//perf/go/culprit/proto/v1/mocks",
         "//perf/go/workflows",
diff --git a/perf/go/workflows/internal/autobisection_service_activity.go b/perf/go/workflows/internal/autobisection_service_activity.go
new file mode 100644
index 0000000..a2db468
--- /dev/null
+++ b/perf/go/workflows/internal/autobisection_service_activity.go
@@ -0,0 +1,30 @@
+package internal
+
+import (
+	"context"
+
+	pb "go.skia.org/infra/perf/go/autobisection/proto/v1"
+	"go.skia.org/infra/perf/go/backend/client"
+)
+
+type AutobisectionServiceActivity struct {
+	insecure_conn bool
+}
+
+func NewAutobisectionServiceActivity() *AutobisectionServiceActivity {
+	return &AutobisectionServiceActivity{
+		insecure_conn: false,
+	}
+}
+
+func (bsa *AutobisectionServiceActivity) SaveAutobisection(ctx context.Context, autobisectionServiceUrl string, req *pb.SaveAutobisectionRequest) (*pb.SaveAutobisectionResponse, error) {
+	apiClient, err := client.NewAutobisectionServiceClient(autobisectionServiceUrl, bsa.insecure_conn)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := apiClient.SaveAutobisection(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
diff --git a/perf/go/workflows/internal/maybe_trigger_bisection.go b/perf/go/workflows/internal/maybe_trigger_bisection.go
index c9754cb..0b315dc 100644
--- a/perf/go/workflows/internal/maybe_trigger_bisection.go
+++ b/perf/go/workflows/internal/maybe_trigger_bisection.go
@@ -9,6 +9,7 @@
 	"go.skia.org/infra/go/metrics2"
 	"go.skia.org/infra/go/skerr"
 	ag_pb "go.skia.org/infra/perf/go/anomalygroup/proto/v1"
+	b_pb "go.skia.org/infra/perf/go/autobisection/proto/v1"
 	c_pb "go.skia.org/infra/perf/go/culprit/proto/v1"
 
 	"go.skia.org/infra/perf/go/types"
@@ -34,6 +35,10 @@
 	return &CulpritServiceActivity{}
 }
 
+func bsaToken() *AutobisectionServiceActivity {
+	return &AutobisectionServiceActivity{}
+}
+
 // MaybeTriggerBisectionWorkflow is the entry point for the workflow which handles anomaly group
 // processing. It is responsible for triggering a bisection if the anomalygroup's
 // group action = BISECT. If group action = REPORT, files a bug notifying user of the anomalies.
@@ -149,7 +154,7 @@
 		return nil, skerr.Wrap(err)
 	}
 
-	if err := processBisectJobResultsPlaceholder(ctx, jobState); err != nil {
+	if err := processBisectJobResults(ctx, jobState, topAnomaly, input.AnomalyGroupId, input.AutobisectionServiceUrl); err != nil {
 		return nil, skerr.Wrap(err)
 	}
 
@@ -475,14 +480,25 @@
 	return resp, nil
 }
 
-func processBisectJobResultsPlaceholder(
+func processBisectJobResults(
 	ctx workflow.Context,
 	jobState *pinpoint.FetchJobStateResponse,
+	anomaly *ag_pb.Anomaly,
+	anomalyGroupId string,
+	autobisectionServiceUrl string,
 ) error {
 	culprits, err := extractCulprits(jobState)
 	if err != nil {
 		return skerr.Wrap(err)
 	}
+
+	autobisectionReq := &b_pb.SaveAutobisectionRequest{
+		JobId:            jobState.JobID,
+		AnomalyGroupId:   anomalyGroupId,
+		AnomalyId:        anomaly.Id,
+		IsRealRegression: isRealRegression(jobState),
+	}
+
 	workflow.GetLogger(ctx).Info(
 		"processBisectJobResults",
 		"WorkflowID",
@@ -494,9 +510,14 @@
 		"Culprits",
 		culprits,
 		"Real regression",
-		isRealRegression(jobState),
+		autobisectionReq.IsRealRegression,
 	)
 
+	// Save the autobisection results to the database.
+	if err := workflow.ExecuteActivity(ctx, bsaToken().SaveAutobisection, autobisectionServiceUrl, autobisectionReq).Get(ctx, nil); err != nil {
+		return skerr.Wrap(err)
+	}
+
 	return nil
 }
 
diff --git a/perf/go/workflows/internal/maybe_trigger_bisection_test.go b/perf/go/workflows/internal/maybe_trigger_bisection_test.go
index 8ef7138..9fb2076 100644
--- a/perf/go/workflows/internal/maybe_trigger_bisection_test.go
+++ b/perf/go/workflows/internal/maybe_trigger_bisection_test.go
@@ -9,6 +9,8 @@
 	"github.com/stretchr/testify/require"
 	anomalygroup_proto "go.skia.org/infra/perf/go/anomalygroup/proto/v1"
 	anomalygroup_mock "go.skia.org/infra/perf/go/anomalygroup/proto/v1/mocks"
+	autobisection_proto "go.skia.org/infra/perf/go/autobisection/proto/v1"
+	autobisection_mock "go.skia.org/infra/perf/go/autobisection/proto/v1/mocks"
 	culprit_proto "go.skia.org/infra/perf/go/culprit/proto/v1"
 
 	"go.skia.org/infra/perf/go/workflows"
@@ -35,9 +37,27 @@
 	}
 }
 
+func setupAutobisectionService(
+	t *testing.T,
+) (string, *autobisection_mock.AutobisectionServiceServer, func()) {
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	s := grpc.NewServer()
+	service := autobisection_mock.NewAutobisectionServiceServer(t)
+	autobisection_proto.RegisterAutobisectionServiceServer(s, service)
+	go func() {
+		require.NoError(t, s.Serve(lis))
+	}()
+	return lis.Addr().String(), service, func() {
+		s.Stop()
+	}
+}
+
 func TestMaybeTriggerBisection_GroupActionBisect_HappyPath(t *testing.T) {
-	addr, server, cleanup := setupAnomalyGroupService(t)
-	defer cleanup()
+	ag_addr, ag_server, ag_cleanup := setupAnomalyGroupService(t)
+	defer ag_cleanup()
+	b_addr, b_server, b_cleanup := setupAutobisectionService(t)
+	defer b_cleanup()
 	c_addr, _, c_cleanup := setupCulpritService(t)
 	defer c_cleanup()
 	testSuite := &testsuite.WorkflowTestSuite{}
@@ -45,9 +65,11 @@
 	agsa := &AnomalyGroupServiceActivity{insecure_conn: true}
 	gsa := &GerritServiceActivity{insecure_conn: true}
 	csa := &CulpritServiceActivity{insecure_conn: true}
+	bsa := &AutobisectionServiceActivity{insecure_conn: true}
 	env.RegisterActivity(agsa)
 	env.RegisterActivity(gsa)
 	env.RegisterActivity(csa)
+	env.RegisterActivity(bsa)
 
 	anomalyGroupId := "group_id1"
 	mockAnomalyIds := []string{"anomaly1"}
@@ -65,7 +87,7 @@
 		},
 		ImprovementDirection: "UP",
 	}
-	server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
+	ag_server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
 		AnomalyGroupId: anomalyGroupId}).
 		Return(
 			&anomalygroup_proto.LoadAnomalyGroupByIDResponse{
@@ -75,7 +97,7 @@
 					AnomalyIds:  mockAnomalyIds,
 				},
 			}, nil)
-	server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
+	ag_server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
 		AnomalyGroupId: anomalyGroupId,
 		Limit:          1}).
 		Return(
@@ -98,13 +120,17 @@
 	env.OnActivity((&pinpoint.Client{}).FetchJobState, mock.Anything, mock.Anything).
 		Return(&pinpoint.FetchJobStateResponse{Status: "completed"}, nil).
 		Once()
-	server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
+	ag_server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
 		Return(
 			&anomalygroup_proto.UpdateAnomalyGroupResponse{}, nil)
+
+	b_server.On("SaveAutobisection", mock.Anything, mock.Anything).Return(&autobisection_proto.SaveAutobisectionResponse{}, nil)
+
 	env.ExecuteWorkflow(MaybeTriggerBisectionWorkflow, &workflows.MaybeTriggerBisectionParam{
-		AnomalyGroupServiceUrl: addr,
-		AnomalyGroupId:         anomalyGroupId,
-		CulpritServiceUrl:      c_addr,
+		AnomalyGroupServiceUrl:  ag_addr,
+		AutobisectionServiceUrl: b_addr,
+		AnomalyGroupId:          anomalyGroupId,
+		CulpritServiceUrl:       c_addr,
 	})
 
 	require.True(t, env.IsWorkflowCompleted())
@@ -115,16 +141,22 @@
 }
 
 func TestMaybeTriggerBisection_GroupActionBisect_ParseChartStat(t *testing.T) {
-	addr, server, cleanup := setupAnomalyGroupService(t)
-	defer cleanup()
+	ag_addr, ag_server, ag_cleanup := setupAnomalyGroupService(t)
+	defer ag_cleanup()
+	b_addr, _, b_cleanup := setupAutobisectionService(t)
+	defer b_cleanup()
+	c_addr, _, c_cleanup := setupCulpritService(t)
+	defer c_cleanup()
 	testSuite := &testsuite.WorkflowTestSuite{}
 	env := testSuite.NewTestWorkflowEnvironment()
 	agsa := &AnomalyGroupServiceActivity{insecure_conn: true}
 	gsa := &GerritServiceActivity{insecure_conn: true}
 	csa := &CulpritServiceActivity{insecure_conn: true}
+	bsa := &AutobisectionServiceActivity{insecure_conn: true}
 	env.RegisterActivity(agsa)
 	env.RegisterActivity(gsa)
 	env.RegisterActivity(csa)
+	env.RegisterActivity(bsa)
 
 	anomalyGroupId := "group_id1"
 	mockAnomalyIds := []string{"anomaly1"}
@@ -142,7 +174,7 @@
 		},
 		ImprovementDirection: "UP",
 	}
-	server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
+	ag_server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
 		AnomalyGroupId: anomalyGroupId}).
 		Return(
 			&anomalygroup_proto.LoadAnomalyGroupByIDResponse{
@@ -152,7 +184,7 @@
 					AnomalyIds:  mockAnomalyIds,
 				},
 			}, nil)
-	server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
+	ag_server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
 		AnomalyGroupId: anomalyGroupId,
 		Limit:          1}).
 		Return(
@@ -175,12 +207,17 @@
 	env.OnActivity((&pinpoint.Client{}).FetchJobState, mock.Anything, mock.Anything).
 		Return(&pinpoint.FetchJobStateResponse{Status: "completed"}, nil).
 		Once()
-	server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
+	ag_server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
 		Return(
 			&anomalygroup_proto.UpdateAnomalyGroupResponse{}, nil)
+
+	env.OnActivity(bsa.SaveAutobisection, mock.Anything, mock.Anything, mock.Anything).Return(&autobisection_proto.SaveAutobisectionResponse{}, nil).Maybe()
+
 	env.ExecuteWorkflow(MaybeTriggerBisectionWorkflow, &workflows.MaybeTriggerBisectionParam{
-		AnomalyGroupServiceUrl: addr,
-		AnomalyGroupId:         anomalyGroupId,
+		AnomalyGroupServiceUrl:  ag_addr,
+		AutobisectionServiceUrl: b_addr,
+		CulpritServiceUrl:       c_addr,
+		AnomalyGroupId:          anomalyGroupId,
 	})
 
 	require.True(t, env.IsWorkflowCompleted())
@@ -193,6 +230,8 @@
 func TestMaybeTriggerBisection_GroupActionReport_HappyPath(t *testing.T) {
 	ag_addr, ag_server, ag_cleanup := setupAnomalyGroupService(t)
 	defer ag_cleanup()
+	b_addr, _, b_cleanup := setupAutobisectionService(t)
+	defer b_cleanup()
 	c_addr, c_server, c_cleanup := setupCulpritService(t)
 	defer c_cleanup()
 	testSuite := &testsuite.WorkflowTestSuite{}
@@ -200,9 +239,11 @@
 	agsa := &AnomalyGroupServiceActivity{insecure_conn: true}
 	gsa := &GerritServiceActivity{insecure_conn: true}
 	csa := &CulpritServiceActivity{insecure_conn: true}
+	bsa := &AutobisectionServiceActivity{insecure_conn: true}
 	env.RegisterActivity(agsa)
 	env.RegisterActivity(gsa)
 	env.RegisterActivity(csa)
+	env.RegisterActivity(bsa)
 
 	anomalyGroupId := "group_id1"
 	mockAnomalyIds := []string{"anomaly1"}
@@ -286,10 +327,13 @@
 	}).Return(
 		&anomalygroup_proto.UpdateAnomalyGroupResponse{}, nil)
 
+	env.OnActivity(bsa.SaveAutobisection, mock.Anything, mock.Anything, mock.Anything).Return(&autobisection_proto.SaveAutobisectionResponse{}, nil).Maybe()
+
 	env.ExecuteWorkflow(MaybeTriggerBisectionWorkflow, &workflows.MaybeTriggerBisectionParam{
-		AnomalyGroupServiceUrl: ag_addr,
-		CulpritServiceUrl:      c_addr,
-		AnomalyGroupId:         anomalyGroupId,
+		AnomalyGroupServiceUrl:  ag_addr,
+		AutobisectionServiceUrl: b_addr,
+		CulpritServiceUrl:       c_addr,
+		AnomalyGroupId:          anomalyGroupId,
 	})
 
 	require.True(t, env.IsWorkflowCompleted())
@@ -302,6 +346,8 @@
 func TestMaybeTriggerBisection_GroupActionBisect_BisectionNotAllowed(t *testing.T) {
 	ag_addr, ag_server, ag_cleanup := setupAnomalyGroupService(t)
 	defer ag_cleanup()
+	b_addr, _, b_cleanup := setupAutobisectionService(t)
+	defer b_cleanup()
 	c_addr, c_server, c_cleanup := setupCulpritService(t)
 	defer c_cleanup()
 	testSuite := &testsuite.WorkflowTestSuite{}
@@ -309,9 +355,11 @@
 	agsa := &AnomalyGroupServiceActivity{insecure_conn: true}
 	gsa := &GerritServiceActivity{insecure_conn: true}
 	csa := &CulpritServiceActivity{insecure_conn: true}
+	bsa := &AutobisectionServiceActivity{insecure_conn: true}
 	env.RegisterActivity(agsa)
 	env.RegisterActivity(gsa)
 	env.RegisterActivity(csa)
+	env.RegisterActivity(bsa)
 
 	anomalyGroupId := "group_id1"
 	mockAnomalyIds := []string{"anomaly1"}
@@ -396,10 +444,13 @@
 	}).Return(
 		&anomalygroup_proto.UpdateAnomalyGroupResponse{}, nil)
 
+	env.OnActivity(bsa.SaveAutobisection, mock.Anything, mock.Anything, mock.Anything).Return(&autobisection_proto.SaveAutobisectionResponse{}, nil).Maybe()
+
 	env.ExecuteWorkflow(MaybeTriggerBisectionWorkflow, &workflows.MaybeTriggerBisectionParam{
-		AnomalyGroupServiceUrl: ag_addr,
-		CulpritServiceUrl:      c_addr,
-		AnomalyGroupId:         anomalyGroupId,
+		AnomalyGroupServiceUrl:  ag_addr,
+		AutobisectionServiceUrl: b_addr,
+		CulpritServiceUrl:       c_addr,
+		AnomalyGroupId:          anomalyGroupId,
 	})
 
 	require.True(t, env.IsWorkflowCompleted())
@@ -410,8 +461,10 @@
 }
 
 func TestMaybeTriggerBisection_GroupActionBisect_HappyPath_StoryNameUpdate(t *testing.T) {
-	addr, server, cleanup := setupAnomalyGroupService(t)
-	defer cleanup()
+	ag_addr, ag_server, ag_cleanup := setupAnomalyGroupService(t)
+	defer ag_cleanup()
+	b_addr, _, b_cleanup := setupAutobisectionService(t)
+	defer b_cleanup()
 	c_addr, _, c_cleanup := setupCulpritService(t)
 	defer c_cleanup()
 	testSuite := &testsuite.WorkflowTestSuite{}
@@ -419,9 +472,11 @@
 	agsa := &AnomalyGroupServiceActivity{insecure_conn: true}
 	gsa := &GerritServiceActivity{insecure_conn: true}
 	csa := &CulpritServiceActivity{insecure_conn: true}
+	bsa := &AutobisectionServiceActivity{insecure_conn: true}
 	env.RegisterActivity(agsa)
 	env.RegisterActivity(gsa)
 	env.RegisterActivity(csa)
+	env.RegisterActivity(bsa)
 
 	anomalyGroupId := "group_id1"
 	mockAnomalyIds := []string{"anomaly1"}
@@ -439,7 +494,7 @@
 		},
 		ImprovementDirection: "UP",
 	}
-	server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
+	ag_server.On("LoadAnomalyGroupByID", mock.Anything, &anomalygroup_proto.LoadAnomalyGroupByIDRequest{
 		AnomalyGroupId: anomalyGroupId}).
 		Return(
 			&anomalygroup_proto.LoadAnomalyGroupByIDResponse{
@@ -449,7 +504,7 @@
 					AnomalyIds:  mockAnomalyIds,
 				},
 			}, nil)
-	server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
+	ag_server.On("FindTopAnomalies", mock.Anything, &anomalygroup_proto.FindTopAnomaliesRequest{
 		AnomalyGroupId: anomalyGroupId,
 		Limit:          1}).
 		Return(
@@ -472,13 +527,17 @@
 	env.OnActivity((&pinpoint.Client{}).FetchJobState, mock.Anything, mock.Anything).
 		Return(&pinpoint.FetchJobStateResponse{Status: "completed"}, nil).
 		Once()
-	server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
+	ag_server.On("UpdateAnomalyGroup", mock.Anything, mock.Anything).
 		Return(
 			&anomalygroup_proto.UpdateAnomalyGroupResponse{}, nil)
+
+	env.OnActivity(bsa.SaveAutobisection, mock.Anything, mock.Anything, mock.Anything).Return(&autobisection_proto.SaveAutobisectionResponse{}, nil).Maybe()
+
 	env.ExecuteWorkflow(MaybeTriggerBisectionWorkflow, &workflows.MaybeTriggerBisectionParam{
-		AnomalyGroupServiceUrl: addr,
-		AnomalyGroupId:         anomalyGroupId,
-		CulpritServiceUrl:      c_addr,
+		AnomalyGroupServiceUrl:  ag_addr,
+		AutobisectionServiceUrl: b_addr,
+		AnomalyGroupId:          anomalyGroupId,
+		CulpritServiceUrl:       c_addr,
 	})
 
 	require.True(t, env.IsWorkflowCompleted())
diff --git a/perf/go/workflows/worker/main.go b/perf/go/workflows/worker/main.go
index 43ff6b8..d6e60a3 100644
--- a/perf/go/workflows/worker/main.go
+++ b/perf/go/workflows/worker/main.go
@@ -62,6 +62,10 @@
 
 	agsa := internal.NewAnomalyGroupServiceActivity()
 	w.RegisterActivity(agsa)
+
+	bsa := internal.NewAutobisectionServiceActivity()
+	w.RegisterActivity(bsa)
+
 	w.RegisterWorkflowWithOptions(
 		internal.MaybeTriggerBisectionWorkflow,
 		workflow.RegisterOptions{Name: workflows.MaybeTriggerBisection},
diff --git a/perf/go/workflows/workflows.go b/perf/go/workflows/workflows.go
index a8f8b00..2f693b1 100644
--- a/perf/go/workflows/workflows.go
+++ b/perf/go/workflows/workflows.go
@@ -27,11 +27,12 @@
 }
 
 type MaybeTriggerBisectionParam struct {
-	AnomalyGroupServiceUrl string
-	CulpritServiceUrl      string
-	AnomalyGroupId         string
-	GroupingTaskQueue      string
-	PinpointTaskQueue      string
+	AnomalyGroupServiceUrl  string
+	AutobisectionServiceUrl string
+	CulpritServiceUrl       string
+	AnomalyGroupId          string
+	GroupingTaskQueue       string
+	PinpointTaskQueue       string
 }
 
 type MaybeTriggerBisectionResult struct {