Skip to content

Commit

Permalink
feat: improve performance when generating highlight card image (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonroberts committed Jul 12, 2023
1 parent 0a0e968 commit 2c94638
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 35 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ DISK_PERCENTAGE=0.7
DISK_SIZE=100
NODE_ENV=development
API_CODENAME=opengraph-local
API_BASE_URL=https://beta.api.opensauced.pizza

# Github
GITHUB_PAT_USER=
Expand Down
12 changes: 3 additions & 9 deletions src/social-card/highlight-card/highlight-card.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class HighlightCardController {
@Get("/:id")
@ApiOperation({
operationId: "generateHighlightSocialCard",
summary: "Gets latest cache aware social card link for :id or generates a new one",
summary: "Generates the social card image for the provided highlight ID",
})
@Header("Content-Type", "image/png")
@ApiOkResponse({ type: StreamableFile, description: "Social card image" })
Expand All @@ -34,15 +34,9 @@ export class HighlightCardController {
@Param("id", ParseIntPipe) id: number,
@Res({ passthrough: true }) res: FastifyReply,
): Promise<void> {
const { fileUrl, hasFile, needsUpdate } = await this.highlightCardService.checkRequiresUpdate(id);
const png = await this.highlightCardService.getHighlightCard(id);

if (hasFile && !needsUpdate) {
return res.status(HttpStatus.FOUND).redirect(fileUrl);
}

const url = await this.highlightCardService.getHighlightCard(id);

return res.status(HttpStatus.FOUND).redirect(url);
return res.status(HttpStatus.OK).send(png);
}

@Get("/:id/metadata")
Expand Down
54 changes: 31 additions & 23 deletions src/social-card/highlight-card/highlight-card.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { HttpService } from "@nestjs/axios";
import { Resvg } from "@resvg/resvg-js";
import { Repository, Language } from "@octokit/graphql-schema";
import fs from "node:fs/promises";
import { firstValueFrom } from "rxjs";

import { GithubService } from "../../github/github.service";
import { S3FileStorageService } from "../../s3-file-storage/s3-file-storage.service";
import userLangs from "../templates/shared/user-langs";
import userProfileRepos from "../templates/shared/user-repos";
import tailwindConfig from "../templates/tailwind.config";
import { firstValueFrom } from "rxjs";
import highlightCardTemplate from "../templates/highlight-card.template";
import { DbUserHighlight } from "../../github/entities/db-user-highlight.entity";
import { DbReaction } from "../../github/entities/db-reaction.entity";
Expand All @@ -32,6 +32,7 @@ interface HighlightCardData {
@Injectable()
export class HighlightCardService {
private readonly logger = new Logger(this.constructor.name);
private fonts: Buffer[] = [];

constructor (
private readonly httpService: HttpService,
Expand All @@ -40,20 +41,20 @@ export class HighlightCardService {
) {}

private async getHighlightData (highlightId: number): Promise<HighlightCardData> {
const highlightReq = await firstValueFrom(
this.httpService.get<DbUserHighlight>(`https://api.opensauced.pizza/v1/user/highlights/${highlightId}`),
const highlightReq = firstValueFrom(
this.httpService.get<DbUserHighlight>(`${process.env.API_BASE_URL!}/v1/user/highlights/${highlightId}`),
);
const { login, updated_at, url, highlight: body } = highlightReq.data;

const reactionsReq = await firstValueFrom(
this.httpService.get<DbReaction[]>(`https://api.opensauced.pizza/v1/highlights/${highlightId}/reactions`),
const reactionsReq = firstValueFrom(
this.httpService.get<DbReaction[]>(`${process.env.API_BASE_URL!}/v1/highlights/${highlightId}/reactions`),
);
const reactions = reactionsReq.data.reduce<number>((acc, curr) => acc + Number(curr.reaction_count), 0);

const [highlight, highlightReactions] = await Promise.all([highlightReq, reactionsReq]);
const { login, updated_at, url, highlight: body } = highlight.data;
const [owner, repoName] = url.replace("https://github.com/", "").split("/");

const user = await this.githubService.getUser(login);
const repo = await this.githubService.getRepo(owner, repoName);
const reactions = highlightReactions.data.reduce<number>((acc, curr) => acc + Number(curr.reaction_count), 0);

const langList = repo.languages?.edges?.flatMap(edge => {
if (edge) {
Expand All @@ -68,7 +69,7 @@ export class HighlightCardService {
body,
login,
reactions,
avatarUrl: `${String(user.avatarUrl)}&size=150`,
avatarUrl: `https://github.com/${login}.png?size=150`,
langs: langList,
langTotal: repo.languages?.totalSize ?? 0,
repo,
Expand All @@ -77,6 +78,17 @@ export class HighlightCardService {
};
}

private async getFonts () {
if (this.fonts.length === 0) {
const interArrayBufferReq = fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff");
const interArrayBufferMediumReq = fs.readFile("node_modules/@fontsource/inter/files/inter-all-500-normal.woff");

this.fonts = await Promise.all([interArrayBufferReq, interArrayBufferMediumReq]);
}

return this.fonts;
}

// public only to be used in local scripts. Not for controller direct use.
async generateCardBuffer (highlightId: number, highlightData?: HighlightCardData) {
const { html } = await import("satori-html");
Expand All @@ -90,8 +102,7 @@ export class HighlightCardService {
highlightCardTemplate(avatarUrl, body, userLangs(langs, langTotal), userProfileRepos([repo], 2), reactions),
);

const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff");
const interArrayBufferMedium = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-500-normal.woff");
const [interArrayBuffer, interArrayBufferMedium] = await this.getFonts();

const svg = await satori(template, {
width: 1200,
Expand Down Expand Up @@ -133,12 +144,16 @@ export class HighlightCardService {
};

if (hasFile) {
const lastModified = await this.s3FileStorageService.getFileLastModified(hash);
const lastModifiedReq = this.s3FileStorageService.getFileLastModified(hash);
const highlightReq = this.getHighlightData(id);
const metadataReq = this.s3FileStorageService.getFileMeta(hash);

const [lastModified, highlight, metadata] = await Promise.all([lastModifiedReq, highlightReq, metadataReq]);

returnVal.lastModified = lastModified;

const { updated_at, reactions } = await this.getHighlightData(id);
const metadata = await this.s3FileStorageService.getFileMeta(hash);
const { updated_at, reactions } = highlight;

const savedReactions = metadata?.["reactions-count"] ?? "0";

if (lastModified && lastModified > updated_at && savedReactions === String(reactions)) {
Expand All @@ -152,7 +167,7 @@ export class HighlightCardService {
return returnVal;
}

async getHighlightCard (id: number): Promise<string> {
async getHighlightCard (id: number): Promise<Buffer> {
const { remaining } = await this.githubService.rateLimit();

if (remaining < 1000) {
Expand All @@ -162,16 +177,9 @@ export class HighlightCardService {
const highlightData = await this.getHighlightData(id);

try {
const hash = `highlights/${String(id)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;

const { png } = await this.generateCardBuffer(id, highlightData);

await this.s3FileStorageService.uploadFile(png, hash, "image/png", { "reactions-count": String(highlightData.reactions) });

this.logger.debug(`Highlight ${id} did not exist in S3, generated image and uploaded to S3, redirecting`);

return fileUrl;
return png;
} catch (e) {
this.logger.error(`Error generating highlight card for ${id}`, e);

Expand Down
4 changes: 2 additions & 2 deletions src/social-card/insight-card/insight-card.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class InsightCardService {
const maxRepoQueryIdsLenght = 10;

const insightPageReq = await firstValueFrom(
this.httpService.get<DbInsight>(`https://api.opensauced.pizza/v1/insights/${insightId}`),
this.httpService.get<DbInsight>(`${process.env.API_BASE_URL!}/v1/insights/${insightId}`),
);

const { repos, name, updated_at } = insightPageReq.data;
Expand All @@ -52,7 +52,7 @@ export class InsightCardService {

const contributorsReq = await firstValueFrom(
this.httpService.get<{ data: { author_login: string }[] }>(
`https://api.opensauced.pizza/v1/contributors/search?${String(query)}`,
`${process.env.API_BASE_URL!}/v1/contributors/search?${String(query)}`,
),
);

Expand Down
2 changes: 1 addition & 1 deletion test/local-dev/HighlightCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
import { mkdir, writeFile } from "fs/promises";
import { HighlightCardService } from "../../src/social-card/highlight-card/highlight-card.service";

const testHighlights = [102, 101, 103, 171];
const testHighlights = [124, 120, 116, 115];

const folderPath = "dist/local-dev";

Expand Down

0 comments on commit 2c94638

Please sign in to comment.