Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ft: Filter by function functionality added to ProfileView #2062

Merged
merged 12 commits into from
Nov 14, 2022
1 change: 1 addition & 0 deletions .github/workflows/go-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ jobs:
uses: golangci/golangci-lint-action@0ad9a0988b3973e851ab0a07adf248ec2e100376 # v3.3.1
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}
args: --timeout 5m
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
run:
deadline: 5m
deadline: 7m
timeout: 5m
go: '1.18'

linters:
Expand Down
582 changes: 297 additions & 285 deletions gen/proto/go/parca/query/v1alpha1/query.pb.go

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions gen/proto/go/parca/query/v1alpha1/query_vtproto.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions gen/proto/swagger/parca/query/v1alpha1/query.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@
"REPORT_TYPE_FLAMEGRAPH_TABLE"
],
"default": "REPORT_TYPE_FLAMEGRAPH_UNSPECIFIED"
},
{
"name": "filterQuery",
"description": "filter_query is the query string to filter the profile samples",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
Expand Down Expand Up @@ -688,6 +695,10 @@
"reportType": {
"$ref": "#/definitions/QueryRequestReportType",
"title": "report_type is the type of report to return"
},
"filterQuery": {
"type": "string",
"title": "filter_query is the query string to filter the profile samples"
}
},
"title": "QueryRequest is a request for a profile query"
Expand Down
29 changes: 29 additions & 0 deletions pkg/query/columnquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"bytes"
"context"
"fmt"
"strings"
"time"

"github.com/go-kit/log"
Expand Down Expand Up @@ -142,9 +143,37 @@ func (q *ColumnQueryAPI) Query(ctx context.Context, req *pb.QueryRequest) (*pb.Q
return nil, err
}

if req.FilterQuery != nil {
p = filterProfileData(p, *req.FilterQuery)
}

return q.renderReport(ctx, p, req.GetReportType())
}

func keepSample(s *profile.SymbolizedSample, filterQuery string) bool {
for _, loc := range s.Locations {
for _, l := range loc.Lines {
if l.Function != nil && strings.Contains(strings.ToLower(l.Function.Name), strings.ToLower(filterQuery)) {
return true
}
}
}
return false
}

func filterProfileData(p *profile.Profile, filterQuery string) *profile.Profile {
filteredSamples := []*profile.SymbolizedSample{}
for _, s := range p.Samples {
if keepSample(s, filterQuery) {
filteredSamples = append(filteredSamples, s)
}
}
return &profile.Profile{
Samples: filteredSamples,
Meta: p.Meta,
}
}

func (q *ColumnQueryAPI) renderReport(ctx context.Context, p *profile.Profile, typ pb.QueryRequest_ReportType) (*pb.QueryResponse, error) {
ctx, span := q.tracer.Start(ctx, "renderReport")
span.SetAttributes(attribute.String("reportType", typ.String()))
Expand Down
28 changes: 28 additions & 0 deletions pkg/query/columnquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ func TestColumnQueryAPIQueryRange(t *testing.T) {
require.Equal(t, 10, len(res.Series[0].Samples))
}

func ptrToString(s string) *string {
return &s
}

func TestColumnQueryAPIQuerySingle(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -304,6 +308,30 @@ func TestColumnQueryAPIQuerySingle(t *testing.T) {
})
require.NoError(t, err)

unfilteredRes, err := api.Query(ctx, &pb.QueryRequest{
ReportType: pb.QueryRequest_REPORT_TYPE_TOP,
Options: &pb.QueryRequest_Single{
Single: &pb.SingleProfile{
Query: `memory:alloc_objects:count:space:bytes{job="default"}`,
Time: ts,
},
},
})
require.NoError(t, err)

filteredRes, err := api.Query(ctx, &pb.QueryRequest{
ReportType: pb.QueryRequest_REPORT_TYPE_TOP,
Options: &pb.QueryRequest_Single{
Single: &pb.SingleProfile{
Query: `memory:alloc_objects:count:space:bytes{job="default", __name__="memory"}`,
Time: ts,
},
},
FilterQuery: ptrToString("runtime"),
})
require.NoError(t, err)
require.Less(t, len(filteredRes.Report.(*pb.QueryResponse_Top).Top.List), len(unfilteredRes.Report.(*pb.QueryResponse_Top).Top.List), "filtered result should be smaller than unfiltered result")

testProf := &pprofpb.Profile{}
err = testProf.UnmarshalVT(MustDecompressGzip(t, res.Report.(*pb.QueryResponse_Pprof).Pprof))
require.NoError(t, err)
Expand Down
3 changes: 3 additions & 0 deletions proto/parca/query/v1alpha1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ message QueryRequest {

// report_type is the type of report to return
ReportType report_type = 5;

// filter_query is the query string to filter the profile samples
optional string filter_query = 6;
}

// Top is the top report type
Expand Down
15 changes: 14 additions & 1 deletion ui/packages/shared/client/src/parca/query/v1alpha1/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,12 @@ export interface QueryRequest {
* @generated from protobuf field: parca.query.v1alpha1.QueryRequest.ReportType report_type = 5;
*/
reportType: QueryRequest_ReportType;
/**
* filter_query is the query string to filter the profile samples
*
* @generated from protobuf field: optional string filter_query = 6;
*/
filterQuery?: string;
}
/**
* Mode is the type of query request
Expand Down Expand Up @@ -1633,7 +1639,8 @@ class QueryRequest$Type extends MessageType<QueryRequest> {
{ no: 2, name: "diff", kind: "message", oneof: "options", T: () => DiffProfile },
{ no: 3, name: "merge", kind: "message", oneof: "options", T: () => MergeProfile },
{ no: 4, name: "single", kind: "message", oneof: "options", T: () => SingleProfile },
{ no: 5, name: "report_type", kind: "enum", T: () => ["parca.query.v1alpha1.QueryRequest.ReportType", QueryRequest_ReportType, "REPORT_TYPE_"] }
{ no: 5, name: "report_type", kind: "enum", T: () => ["parca.query.v1alpha1.QueryRequest.ReportType", QueryRequest_ReportType, "REPORT_TYPE_"] },
{ no: 6, name: "filter_query", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<QueryRequest>): QueryRequest {
Expand Down Expand Up @@ -1672,6 +1679,9 @@ class QueryRequest$Type extends MessageType<QueryRequest> {
case /* parca.query.v1alpha1.QueryRequest.ReportType report_type */ 5:
message.reportType = reader.int32();
break;
case /* optional string filter_query */ 6:
message.filterQuery = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
Expand Down Expand Up @@ -1699,6 +1709,9 @@ class QueryRequest$Type extends MessageType<QueryRequest> {
/* parca.query.v1alpha1.QueryRequest.ReportType report_type = 5; */
if (message.reportType !== 0)
writer.tag(5, WireType.Varint).int32(message.reportType);
/* optional string filter_query = 6; */
if (message.filterQuery !== undefined)
writer.tag(6, WireType.LengthDelimited).string(message.filterQuery);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
Expand Down
1 change: 1 addition & 0 deletions ui/packages/shared/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@headlessui/react": "^1.4.3",
"@heroicons/react": "^1.0.5",
"@iconify/react": "^3.2.2",
"@parca/client": "^0.16.54",
"@parca/dynamicsize": "^0.16.51",
"@parca/functions": "^0.16.51",
Expand Down
2 changes: 1 addition & 1 deletion ui/packages/shared/components/src/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const Button = ({
className={cx(
disabled ? 'opacity-50 pointer-events-none' : '',
...Object.values(BUTTON_VARIANT[variant]),
'cursor-pointer group relative w-full flex $ text-sm rounded-md text-whitefocus:outline-none focus:ring-2 focus:ring-offset-2',
'cursor-pointer group relative w-full flex $ text-sm rounded-md text-whitefocus:outline-none focus:ring-2 focus:ring-offset-2 items-center justify-center',
className
)}
disabled={disabled}
Expand Down
71 changes: 61 additions & 10 deletions ui/packages/shared/components/src/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,70 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {Icon} from '@iconify/react';
import Button from '../Button';
import cx from 'classnames';
import {useRef} from 'react';

const Input = ({className = '', ...props}): JSX.Element => {
interface SelfProps {
className?: string;
onAction?: () => void;
actionIcon?: JSX.Element;
}

export type Props = React.InputHTMLAttributes<HTMLInputElement> & SelfProps;

const Input = ({
className = '',
onAction,
actionIcon = <Icon icon="ep:arrow-right" />,
onBlur,
...props
}: Props): JSX.Element => {
const ref = useRef<HTMLInputElement>(null);
return (
<input
{...props}
className={cx(
'p-2 rounded-md bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-600',
{
[className]: className.length > 0,
}
)}
/>
<div
className="relative"
ref={ref}
onBlur={e => {
(async () => {
if (onBlur == null || ref.current == null) {
return;
}
await new Promise(resolve => setTimeout(resolve));
if (ref.current.contains(document.activeElement)) {
return;
}
onBlur(e as React.FocusEvent<HTMLInputElement>);
})().catch(err => {
console.error('Error in processing blur event', err);
});
}}
>
<input
{...props}
className={cx(
'p-2 rounded-md bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-600',
{
[className]: className.length > 0,
'pr-10': onAction != null,
}
)}
onKeyDown={e => {
if (e.key === 'Enter' && onAction != null) {
onAction();
}
}}
/>
{onAction != null ? (
<Button
onClick={onAction}
className="!absolute w-fit inset-y-0 right-0 !px-2 aspect-square rounded-tl-none rounded-bl-none"
>
{actionIcon}
</Button>
) : null}
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ A Component to select a range of dates with time.
</Story>
</Preview>

### With Action

<Preview>
<Story name="type=number">
<div className="p-8">
<Input type="number" defaultValue="1" onAction={() => {}}/>
</div>
</Story>
</Preview>

## Props

<Props of={Input} />
8 changes: 7 additions & 1 deletion ui/packages/shared/components/src/SearchNodes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import {Input} from '../';
import {useEffect, useMemo} from 'react';
import {useAppDispatch, setSearchNodeString} from '@parca/store';
import useUIFeatureFlag from '@parca/functions/useUIFeatureFlag';
import {debounce} from 'lodash';

const SearchNodes = (): JSX.Element => {
const dispatch = useAppDispatch();
const [filterByFunctionEnabled] = useUIFeatureFlag('filterByFunction');

useEffect(() => {
return () => {
Expand All @@ -35,7 +37,11 @@ const SearchNodes = (): JSX.Element => {

return (
<div>
<Input className="text-sm" placeholder="Search nodes..." onChange={debouncedSearch}></Input>
<Input
className="text-sm"
placeholder={filterByFunctionEnabled ? 'Highlight nodes...' : 'Search nodes...'}
onChange={debouncedSearch}
></Input>
</div>
);
};
Expand Down
Loading