From d7fef8d89ef3616053b73e15eee3d15a85a2668a Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Fri, 18 Mar 2022 12:52:42 -0700 Subject: [PATCH] Profiling endpoint fixes (#1707) - Added support to download all profile tests - profile.zip file was corrupted after download - Add suspension warning - Add Checkbox support - Support for running multiple profiling types at the same time - fix profiling test Signed-off-by: Lenin Alevski --- models/profiler_type.go | 110 ---------- models/profiling_start_request.go | 44 +--- .../src/screens/Console/Support/Profile.tsx | 199 ++++++++++++++++++ portal-ui/src/screens/Console/Tools/Tools.tsx | 2 + portal-ui/src/screens/Console/valid-routes.ts | 27 +-- restapi/admin_profiling.go | 9 +- restapi/admin_profiling_test.go | 3 +- restapi/doc.go | 1 + restapi/embedded_spec.go | 32 +-- restapi/operations/console_api.go | 12 ++ swagger-console.yml | 14 +- 11 files changed, 241 insertions(+), 212 deletions(-) delete mode 100644 models/profiler_type.go create mode 100644 portal-ui/src/screens/Console/Support/Profile.tsx diff --git a/models/profiler_type.go b/models/profiler_type.go deleted file mode 100644 index 9214ffd39..000000000 --- a/models/profiler_type.go +++ /dev/null @@ -1,110 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -// This file is part of MinIO Console Server -// Copyright (c) 2022 MinIO, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - "encoding/json" - - "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/validate" -) - -// ProfilerType profiler type -// -// swagger:model profilerType -type ProfilerType string - -func NewProfilerType(value ProfilerType) *ProfilerType { - return &value -} - -// Pointer returns a pointer to a freshly-allocated ProfilerType. -func (m ProfilerType) Pointer() *ProfilerType { - return &m -} - -const ( - - // ProfilerTypeCPU captures enum value "cpu" - ProfilerTypeCPU ProfilerType = "cpu" - - // ProfilerTypeMem captures enum value "mem" - ProfilerTypeMem ProfilerType = "mem" - - // ProfilerTypeBlock captures enum value "block" - ProfilerTypeBlock ProfilerType = "block" - - // ProfilerTypeMutex captures enum value "mutex" - ProfilerTypeMutex ProfilerType = "mutex" - - // ProfilerTypeTrace captures enum value "trace" - ProfilerTypeTrace ProfilerType = "trace" - - // ProfilerTypeThreads captures enum value "threads" - ProfilerTypeThreads ProfilerType = "threads" - - // ProfilerTypeGoroutines captures enum value "goroutines" - ProfilerTypeGoroutines ProfilerType = "goroutines" -) - -// for schema -var profilerTypeEnum []interface{} - -func init() { - var res []ProfilerType - if err := json.Unmarshal([]byte(`["cpu","mem","block","mutex","trace","threads","goroutines"]`), &res); err != nil { - panic(err) - } - for _, v := range res { - profilerTypeEnum = append(profilerTypeEnum, v) - } -} - -func (m ProfilerType) validateProfilerTypeEnum(path, location string, value ProfilerType) error { - if err := validate.EnumCase(path, location, value, profilerTypeEnum, true); err != nil { - return err - } - return nil -} - -// Validate validates this profiler type -func (m ProfilerType) Validate(formats strfmt.Registry) error { - var res []error - - // value enum - if err := m.validateProfilerTypeEnum("", "body", m); err != nil { - return err - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -// ContextValidate validates this profiler type based on context it is used -func (m ProfilerType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} diff --git a/models/profiling_start_request.go b/models/profiling_start_request.go index 8a84938f1..1f21b7784 100644 --- a/models/profiling_start_request.go +++ b/models/profiling_start_request.go @@ -38,7 +38,7 @@ type ProfilingStartRequest struct { // type // Required: true - Type *ProfilerType `json:"type"` + Type *string `json:"type"` } // Validate validates this profiling start request @@ -61,51 +61,11 @@ func (m *ProfilingStartRequest) validateType(formats strfmt.Registry) error { return err } - if err := validate.Required("type", "body", m.Type); err != nil { - return err - } - - if m.Type != nil { - if err := m.Type.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("type") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("type") - } - return err - } - } - return nil } -// ContextValidate validate this profiling start request based on the context it is used +// ContextValidate validates this profiling start request based on context it is used func (m *ProfilingStartRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - var res []error - - if err := m.contextValidateType(ctx, formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *ProfilingStartRequest) contextValidateType(ctx context.Context, formats strfmt.Registry) error { - - if m.Type != nil { - if err := m.Type.ContextValidate(ctx, formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("type") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("type") - } - return err - } - } - return nil } diff --git a/portal-ui/src/screens/Console/Support/Profile.tsx b/portal-ui/src/screens/Console/Support/Profile.tsx new file mode 100644 index 000000000..854bfa8af --- /dev/null +++ b/portal-ui/src/screens/Console/Support/Profile.tsx @@ -0,0 +1,199 @@ +import React, { Fragment, useState } from "react"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import { Button, Grid } from "@mui/material"; +import PageHeader from "../Common/PageHeader/PageHeader"; +import PageLayout from "../Common/Layout/PageLayout"; +import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper"; +import api from "../../../common/api"; +import { ErrorResponseHandler } from "../../../common/types"; +import { + actionsTray, + containerForHeader, + inlineCheckboxes, +} from "../Common/FormComponents/common/styleLibrary"; +import HelpBox from "../../../common/HelpBox"; +import WarnIcon from "../../../icons/WarnIcon"; + +const styles = (theme: Theme) => + createStyles({ + buttonContainer: { + marginTop: 24, + textAlign: "right", + "& .MuiButton-root": { + marginLeft: 8, + }, + }, + dropdown: { + marginBottom: 24, + }, + checkboxLabel: { + marginTop: 12, + marginRight: 4, + fontSize: 16, + fontWeight: 500, + }, + checkboxDisabled: { + opacity: 0.5, + }, + inlineCheckboxes: { + ...inlineCheckboxes.inlineCheckboxes, + alignItems: "center", + + "@media (max-width: 900px)": { + flexFlow: "column", + alignItems: "flex-start", + }, + }, + ...actionsTray, + ...containerForHeader(theme.spacing(4)), + }); + +interface IProfileProps { + classes: any; +} + +const Profile = ({ classes }: IProfileProps) => { + const [profilingStarted, setProfilingStarted] = useState(false); + const [types, setTypes] = useState([ + "cpu", + "mem", + "block", + "mutex", + "trace", + "threads", + "goroutines", + ]); + + const typesList = [ + { label: "cpu", value: "cpu" }, + { label: "mem", value: "mem" }, + { label: "block", value: "block" }, + { label: "mutex", value: "mutex" }, + { label: "trace", value: "trace" }, + { label: "threads", value: "threads" }, + { label: "goroutines", value: "goroutines" }, + ]; + + const onCheckboxClick = (e: React.ChangeEvent) => { + let newArr: string[] = []; + if (types.indexOf(e.target.value) > -1) { + newArr = types.filter((type) => type !== e.target.value); + } else { + newArr = [...types, e.target.value]; + } + setTypes(newArr); + }; + + const startProfiling = () => { + if (!profilingStarted) { + const typeString = types.join(","); + setProfilingStarted(true); + api + .invoke("POST", `/api/v1/profiling/start`, { + type: typeString, + }) + .then(() => {}) + .catch((err: ErrorResponseHandler) => { + console.log(err); + setProfilingStarted(false); + }); + } + }; + + const stopProfiling = () => { + if (profilingStarted) { + const anchor = document.createElement("a"); + document.body.appendChild(anchor); + let path = "/api/v1/profiling/stop"; + var req = new XMLHttpRequest(); + req.open("POST", path, true); + req.responseType = "blob"; + req.onreadystatechange = () => { + if (req.readyState === 4 && req.status === 200) { + let filename = "profile.zip"; + setProfilingStarted(false); + var link = document.createElement("a"); + link.href = window.URL.createObjectURL(req.response); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; + req.send(); + } + }; + + return ( + + + + + + +
Types to profile:
+ {typesList.map((t) => ( + -1} + disabled={profilingStarted} + key={`checkbox-${t.label}`} + id={`checkbox-${t.label}`} + label={t.label} + name={`checkbox-${t.label}`} + onChange={onCheckboxClick} + value={t.value} + /> + ))} +
+
+ + + + +
+ {!profilingStarted && ( + +
+ } + help={} + /> + + )} +
+
+ ); +}; + +export default withStyles(styles)(Profile); diff --git a/portal-ui/src/screens/Console/Tools/Tools.tsx b/portal-ui/src/screens/Console/Tools/Tools.tsx index 834c14d7a..fc4c4071d 100644 --- a/portal-ui/src/screens/Console/Tools/Tools.tsx +++ b/portal-ui/src/screens/Console/Tools/Tools.tsx @@ -27,6 +27,7 @@ import withSuspense from "../Common/Components/withSuspense"; const Inspect = withSuspense(React.lazy(() => import("./Inspect"))); const Register = withSuspense(React.lazy(() => import("../Support/Register"))); +const Profile = withSuspense(React.lazy(() => import("../Support/Profile"))); const Tools = () => { return ( @@ -34,6 +35,7 @@ const Tools = () => { +