Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

함수 성능과 자원 사용량

함수의 비용과 지연은 함수 본문, 입력 객체 로드 및 온톨로지나 모델 호출에서 발생합니다. 전체 객체를 반복해서 읽기 전에 백엔드 필터와 집계로 계산할 수 있는지 확인합니다.

실행 비용과 측정

함수 실행에는 호출마다 고정 4 compute-seconds의 오버헤드, 코드의 vCPU 실행 시간, 온톨로지 질의와 모델 및 LLM 같은 외부 호출의 비용이 있습니다. 4 compute-seconds는 벽시계 지연 4초라는 뜻이 아닙니다. 온톨로지 질의 사용량과 함께 확인합니다.

Performance 탭의 waterfall은 시간축 위에 작업별 막대를 표시합니다.

Execute function은 코드의 CPU 시간, Load objects from arguments와 Load objects from links는 객체 저장소 서비스(OSS)를 호출하는 시간을 나타냅니다. 백엔드 호출이 직렬로 이어지는지, 불필요한 중첩 반복이 있는지 구분해야 합니다.

입력 선택

입력사용 조건과 비용
객체 집합필터와 집계 정의를 전달하고 필요한 시점까지 객체 로드를 미룹니다. 집계값만 필요하면 개별 객체를 로드하지 않습니다.
객체 배열지정한 객체들을 실행 환경에 미리 로드하며, 실제 객체별 처리에 사용할 수 있습니다.
단일 객체한 업무 객체를 처리할 때 사용합니다. 여러 행 작업을 이 함수의 반복 호출로 구성하면 객체 로드와 호출 고정비가 반복됩니다.

단일 객체 입력도 객체 데이터를 읽는 요청을 발생시킵니다. 예를 들어 이름을 반환하려면 객체 속성을 읽어야 하지만, 객체 수만 필요하다면 집합의 count를 사용합니다.

키별 반복 조회를 집합 합계로 변경

키 하나마다 조회하면 N개 키에 N번의 왕복이 필요합니다. 키 목록을 한 조건으로 검색하고 객체를 반복하면 요청 수를 줄일 수 있으며, 합계만 필요하면 서버 집계로 객체 로드도 피할 수 있습니다.

아래 가상의 ExampleDataAircraft는 검색 가능한 tailNumber와 숫자 numberOfSeats를 가집니다. 입력 키 목록은 집합으로 해석하여 같은 키가 여러 번 있어도 객체 하나를 한 번만 합산합니다. 좌석 수가 없는 객체는 합계에 기여하지 않으며 빈 결과의 합계는 0입니다.

TSv1은 빈 exactMatch 배열을 전달하면 필터가 무시되므로 빈 입력을 검색 전에 처리해야 합니다.

import { Double, Function } from "@foundry/functions-api";
import { Objects } from "@foundry/ontology-api";

export class AircraftAggregationFunctions {
    @Function()
    public async seatsForAircraft(keys: string[]): Promise<Double> {
        if (keys.length === 0) return 0;
        const total = await Objects.search().exampleDataAircraft()
            .filter(aircraft => aircraft.tailNumber.exactMatch(...keys))
            .sum(aircraft => aircraft.numberOfSeats);
        return total ?? 0;
    }
}

TSv2 seatsForAircraft.ts는 $in으로 검색한 뒤 속성 합계를 읽습니다.

import { Client } from "@osdk/client";
import { Double } from "@osdk/functions";
import { ExampleDataAircraft } from "@ontology/sdk";

export default async function seatsForAircraft(client: Client, keys: string[]): Promise<Double> {
    if (keys.length === 0) return 0;
    const result = await client(ExampleDataAircraft).where({ tailNumber: { $in: keys } })
        .aggregate({ $select: { "numberOfSeats:sum": "unordered" } });
    return result.numberOfSeats.sum ?? 0;
}

Python은 in_과 sum을 사용합니다.

from functions.api import Double, function
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import ExampleDataAircraft


@function
def seats_for_aircraft(keys: list[str]) -> Double:
    if not keys:
        return 0.0
    result = (FoundryClient().ontology.objects.ExampleDataAircraft
              .where(ExampleDataAircraft.object_type.tail_number.in_(keys))
              .sum(ExampleDataAircraft.object_type.number_of_seats).compute())
    return 0.0 if result is None else float(result)

개별 조회에서 중복 입력마다 같은 좌석 수를 다시 더하던 로직을 옮기면 의미가 달라집니다. 중복 횟수를 가중치로 쓰는 업무는 그 가중치를 별도로 유지해야 합니다.

링크 조회의 병렬화와 비용

employee.workHistory.all()이나 TSv2 링크의 fetchPage를 반복문에서 차례로 기다리면 직원마다 왕복을 수행합니다. 독립적인 요청은 Promise.all로 함께 기다릴 수 있습니다.

Promise.all은 같은 요청 수의 대기 시간을 겹치게 합니다. 요청 수와 플랫폼 연산 비용을 줄이는 것은 아니며, 순수 JavaScript 계산에 async를 붙여도 CPU 계산을 병렬화하지는 않습니다. 단순 나눗셈을 Promise로 포장하면 오히려 객체와 Promise를 더 만들게 됩니다.

TSv2에서 프로젝트 객체를 전부 로드하지 않고 각 직원의 연결 개수를 동시에 조회하는 getEmployeeProjectCount.ts는 다음과 같습니다.

import { ObjectSpecifier, Osdk } from "@osdk/client";
import { Double } from "@osdk/functions";
import { Employee } from "@ontology/sdk";

export default async function getEmployeeProjectCount(
    employees: Osdk.Instance<Employee>[]
): Promise<Record<ObjectSpecifier<Employee>, Double>> {
    const counts = await Promise.all(employees.map(async employee => ({
        employee,
        result: await employee.$link.workHistory.aggregate({ $select: { $count: "unordered" } }),
    })));
    const result: Record<ObjectSpecifier<Employee>, Double> = {};
    for (const item of counts) result[item.employee.$objectSpecifier] = item.result.$count;
    return result;
}

이 함수도 직원 수만큼 집계를 호출합니다. 관계를 나타내는 외래 키로 한꺼번에 그룹화할 수 있으면 더 적은 요청으로 계산할 수 있습니다.

직원별 프로젝트를 한 번에 집계

다음 가상 Employee의 기본 키는 Integer형 id이며, Project의 Integer형 employeeId 속성에 이 키를 저장한다고 가정합니다. 두 속성은 Searchable이며 입력에 서로 다른 직원 ID가 최대 10,000개 들어오는 작업에 사용합니다.

TSv1은 너비 1의 숫자 버킷을 사용하여 정수 ID별로 집계합니다. 기본 키 속성 id와 버킷의 min을 대응시킵니다.

import { Double, Function, FunctionsMap, UserFacingError } from "@foundry/functions-api";
import { Employee, Objects } from "@foundry/ontology-api";

export class ProjectCountFunctions {
    @Function()
    public async getEmployeeProjectCount(employees: Employee[]): Promise<FunctionsMap<Employee, Double>> {
        const result = new FunctionsMap<Employee, Double>();
        if (employees.length === 0) return result;
        const ids = [...new Set(employees.map(employee => employee.id))];
        if (ids.length > 10_000) throw new UserFacingError("직원 집계를 10,000개 ID 이하로 나누어야 합니다.");
        const aggregation = await Objects.search().project()
            .filter(project => project.employeeId.exactMatch(...ids))
            .groupBy(project => project.employeeId.byFixedWidth(1)).count();
        const counts = new Map<number, Double>();
        for (const bucket of aggregation.buckets) {
            if (bucket.key.min === undefined) throw new Error("직원 ID 버킷의 시작이 없습니다.");
            counts.set(bucket.key.min, bucket.value);
        }
        for (const employee of employees) result.set(employee, counts.get(employee.id) ?? 0);
        return result;
    }

    @Function()
    public async findEmployeeWithMostProjects(employees: Employee[]): Promise<Employee | undefined> {
        if (employees.length === 0) return undefined;
        const counts = await this.getEmployeeProjectCount(employees);
        let selected = employees[0]!;
        let maximum = counts.get(selected) ?? 0;
        for (let index = 1; index < employees.length; index++) {
            const employee = employees[index]!;
            const count = counts.get(employee) ?? 0;
            if (count > maximum) {
                selected = employee;
                maximum = count;
            }
        }
        return selected;
    }
}

프로젝트가 없는 직원은 0이며 최대값이 같으면 입력에서 먼저 나온 직원을 선택합니다. 빈 직원 목록은 선택 객체가 없으므로 undefined를 반환합니다.

문자열 ID에 topValues를 사용하면 상위 1,000값의 한도와 근사가 적용됩니다. 전체 집계는 최대 10,000버킷이므로 더 큰 대상은 나눠 계산하거나 다른 데이터 처리 경로를 사용합니다.

연결 집합을 한 번에 탐색

모든 항공편의 정비 이벤트를 한꺼번에 세려면 먼저 집합에서 링크를 탐색한 뒤 count합니다.

import { ObjectSet } from "@osdk/client";
import { Double } from "@osdk/functions";
import { ExampleDataAircraft } from "@ontology/sdk";

export default async function bulkProcessing(aircraft: ObjectSet<ExampleDataAircraft>): Promise<Double> {
    const events = aircraft.pivotTo("exampleDataAircraftMaintenanceEvent");
    return (await events.aggregate({ $select: { $count: "unordered" } })).$count;
}

TSv1은 집합의 searchAroundExampleDataAircraftMaintenanceEvent().count(), Python은 search_around_example_data_aircraft_maintenance_event().count().compute()에 해당합니다. 정확한 링크 API 이름과 생성 메서드는 자신의 스키마를 사용합니다.

결과는 연결된 고유 이벤트 객체 수입니다. 하나의 이벤트가 두 항공기에 연결되어도 집합에서는 한 번 셉니다. 항공기별 개수를 따로 더한 값은 같은 이벤트를 여러 번 셀 수 있으므로 두 계산이 같은 업무 지표인지 확인해야 합니다.

편집도 페이지로 읽기

편집할 객체를 하나씩 검색하는 대신 입력 집합을 반복합니다. TSv2의 asyncIter와 Python의 iterate 또는 명시적 page로 조회 메모리를 제한할 수 있습니다.

from functions.api import OntologyEdit, function
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import ExampleDataAircraft
from ontology_sdk.ontology.object_sets import ExampleDataAircraftObjectSet


@function(edits=[ExampleDataAircraft])
def edit_all_aircrafts(aircraft: ExampleDataAircraftObjectSet) -> list[OntologyEdit]:
    edits = FoundryClient().ontology.edits()
    for item in aircraft.iterate():
        edits.objects.ExampleDataAircraft.edit(item).display_name = "new display name"
    return edits.get_edits()


@function(edits=[ExampleDataAircraft])
def edit_all_with_pagination(aircraft: ExampleDataAircraftObjectSet) -> list[OntologyEdit]:
    edits = FoundryClient().ontology.edits()
    next_token = None
    while True:
        page = aircraft.page(1000, next_token)
        for item in page.data:
            edits.objects.ExampleDataAircraft.edit(item).status = "reviewed"
        next_token = page.next_page_token
        if not next_token:
            break
    return edits.get_edits()

TSv2는 각 객체에 batch.update를 적용하고 batch.getEdits를 반환합니다. TSv1은 객체 배열이나 all/allAsync 결과를 수정하므로 전체 로드 한도가 남습니다.

페이지별 읽기여도 최종 편집 목록은 함수에 쌓입니다. 객체 로드 메모리를 줄였다는 이유로 액션 편집 한도나 함수 메모리 한도를 넘길 수는 없습니다.

Workshop 함수 기반 열

Workshop은 표의 현재 수십 행을 객체 집합으로 넘기고, 함수는 객체별 표시 값을 맵으로 반환할 수 있습니다. 코드 없이 Workshop에서 설정하는 파생 속성과 함수 기반 열은 다릅니다.

가상의 ObjectTypeA.exampleProperty는 Double입니다. 예제 계산은 이를 scalar로 나눕니다. 속성이 없거나 scalar가 0이면 해당 객체의 계산값을 생략하지만 값이 0인 속성과 결과는 보존합니다.

TSv2는 필요한 속성만 $select로 읽습니다.

import { ObjectSet, ObjectSpecifier } from "@osdk/client";
import { Double } from "@osdk/functions";
import { ObjectTypeA } from "@ontology/sdk";

export default async function derivedColumn(
    objects: ObjectSet<ObjectTypeA>, scalar: Double
): Promise<Record<ObjectSpecifier<ObjectTypeA>, Double>> {
    const result: Record<ObjectSpecifier<ObjectTypeA>, Double> = {};
    if (scalar === 0) return result;
    for await (const object of objects.asyncIter({ $select: ["exampleProperty"] })) {
        if (object.exampleProperty !== undefined) result[object.$objectSpecifier] = object.exampleProperty / scalar;
    }
    return result;
}

TSv1도 truthiness 대신 undefined를 검사합니다.

import { Double, Function, FunctionsMap } from "@foundry/functions-api";
import { ObjectSet, ObjectTypeA } from "@foundry/ontology-api";

export class ColumnFunctions {
    @Function()
    public async derivedColumn(objects: ObjectSet<ObjectTypeA>, scalar: Double): Promise<FunctionsMap<ObjectTypeA, Double>> {
        const result = new FunctionsMap<ObjectTypeA, Double>();
        if (scalar === 0) return result;
        for (const object of await objects.allAsync()) {
            if (object.exampleProperty !== undefined) result.set(object, object.exampleProperty / scalar);
        }
        return result;
    }
}

if(result)나 if(object.exampleProperty)를 사용하면 유효한 0도 누락됩니다. 이 계산은 CPU 나눗셈만 수행하므로 Promise.all로 감쌀 이유가 없습니다.

객체별 원격 지표 계산

추가 온톨로지 질의가 필요한 계산은 독립적인 요청을 병렬로 시작할 수 있습니다. 다음 가상 ObjectTypeB는 property, metric, propertyToAggregateA와 propertyToAggregateB를 가집니다. ObjectTypeA.exampleProperty와 같은 property를 검색하고 다음 비율을 계산합니다.

  • 분자: 일치하는 모든 B의 propertyToAggregateB 합계
  • 분모: metric이 존재하는 B의 propertyToAggregateA 합계

합계가 없는 경우 0으로 해석하며 분모가 0이면 계산값을 생략하는 예제 업무 규칙입니다. 이 규칙을 앱의 의미에 맞춰 사용합니다.

import { Client, ObjectSet, ObjectSpecifier } from "@osdk/client";
import { Double } from "@osdk/functions";
import { ObjectTypeA, ObjectTypeB } from "@ontology/sdk";

async function computeMetric(objects: ObjectSet<ObjectTypeB>): Promise<Double | undefined> {
    const [numeratorResult, denominatorResult] = await Promise.all([
        objects.aggregate({ $select: { "propertyToAggregateB:sum": "unordered" } }),
        objects.where({ metric: { $isNull: false } })
            .aggregate({ $select: { "propertyToAggregateA:sum": "unordered" } }),
    ]);
    const numerator = numeratorResult.propertyToAggregateB.sum ?? 0;
    const denominator = denominatorResult.propertyToAggregateA.sum ?? 0;
    return denominator === 0 ? undefined : numerator / denominator;
}

export default async function derivedRemoteColumn(
    client: Client, objects: ObjectSet<ObjectTypeA>
): Promise<Record<ObjectSpecifier<ObjectTypeA>, Double>> {
    const result: Record<ObjectSpecifier<ObjectTypeA>, Double> = {};
    const selectedObjects = await Array.fromAsync(objects.asyncIter({ $select: ["exampleProperty"] }));
    await Promise.all(selectedObjects.map(async object => {
        const value = object.exampleProperty;
        if (value === undefined) return;
        const metric = await computeMetric(client(ObjectTypeB).where({ property: { $eq: value } }));
        if (metric !== undefined) result[object.$objectSpecifier] = metric;
    }));
    return result;
}

이 패턴은 Workshop의 제한된 표시 행을 대상으로 합니다. 모든 객체의 요청을 한꺼번에 만들면 동시 요청과 메모리 사용도 늘어납니다. 많은 행에는 먼저 그룹 집계로 요청 수를 줄이거나 작업 범위를 나누어야 합니다.

TSv1에서 같은 지표를 계산하면 sum이 버킷 객체가 아니라 숫자 또는 undefined를 반환한다는 점에 주의합니다.

import { Double, Function } from "@foundry/functions-api";
import { ObjectSet, ObjectTypeB } from "@foundry/ontology-api";

export class MetricFunctions {
    @Function()
    public async computeMetric(objects: ObjectSet<ObjectTypeB>): Promise<Double | undefined> {
        const [sum, conditionalSum] = await Promise.all([
            objects.sum(object => object.propertyToAggregateB),
            objects.filter(object => object.metric.hasProperty()).sum(object => object.propertyToAggregateA),
        ]);
        const denominator = conditionalSum ?? 0;
        return denominator === 0 ? undefined : (sum ?? 0) / denominator;
    }
}

차트용 집계 반환

같은 계산 결과를 Chart XY에 쓰려면 객체별 맵 대신 TwoDimensionalAggregation을 반환합니다. 가상 ObjectTypeA.pkProperty는 String 기본 키입니다.

TSv1에서는 기존 계산 맵을 사용하고 명시적 기본 키 속성을 차트 범주로 전달합니다.

import { Double, Function, FunctionsMap, TwoDimensionalAggregation } from "@foundry/functions-api";
import { ObjectTypeA } from "@foundry/ontology-api";

export class ChartFunctions {
    @Function()
    public asChart(values: FunctionsMap<ObjectTypeA, Double>): TwoDimensionalAggregation<string, Double> {
        const buckets: { key: string; value: Double }[] = [];
        for (const [object, value] of values.entries()) buckets.push({ key: object.pkProperty, value });
        return { buckets };
    }
}

TSv2는 바깥 buckets 래퍼 없이 버킷 배열을 반환합니다. 객체 집합에서 계산하며 바로 버킷을 만들 수도 있습니다.

import { ObjectSet } from "@osdk/client";
import { Double, TwoDimensionalAggregation } from "@osdk/functions";
import { ObjectTypeA } from "@ontology/sdk";

export default async function derivedChart(
    objects: ObjectSet<ObjectTypeA>, scalar: Double
): Promise<TwoDimensionalAggregation<string, Double>> {
    const buckets: TwoDimensionalAggregation<string, Double> = [];
    if (scalar === 0) return buckets;
    for await (const object of objects.asyncIter({ $select: ["exampleProperty"] })) {
        if (object.exampleProperty !== undefined) {
            buckets.push({ key: String(object.$primaryKey), value: object.exampleProperty / scalar });
        }
    }
    return buckets;
}

함수 자료형과 TSv2 이전에서 맵, 선택 값, 집계 구조 및 검색 한도의 차이를 확인합니다.