1
votes

Currently I'm working on an Angular 8 application which uses Firebase as backend.

I followed Jeff Delaney's Tutorial and suceeded to deploy on a cloud function called ssr my express server.

Everything works fine... almost ! When the ssr function is deployed, I can see an increase of memory usage of all my cloud functions even the smallest (1 transaction with a get and 1 update in the transaction) :

functions/src/index.ts

// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
import * as functions from 'firebase-functions';
// The Firebase Admin SDK to access Firestore.
import * as admin from 'firebase-admin';

admin.initializeApp();

export const ssr = functions.https.onRequest(require(`${process.cwd()}/dist/server`).app);

export const unsubscribeToMeal = functions.region('europe-west1').https.onCall((data) => {
  const mealDoc = admin.firestore().doc('meals/' + data.meal.uid);
  const adminDoc = admin.firestore().doc('users/' + data.meal.adminId);
  return admin.firestore().runTransaction(t => {
    return t.get(mealDoc)
      .then(doc => {
        const meal = doc.data();
        if (doc.exists && meal) {
          const promises = [];
          const participantIndex = meal.users.findIndex((element: any) => {
            return element.detail.uid === data.userToUnsubscribe.uid;
          });
          meal.users.splice(participantIndex, 1);
          const pendingRequest = meal.users.filter((user: any) => user.status === 'pending').length > 0;
          const p4 = t.update(mealDoc, { 'users': meal.users, 'participantId': admin.firestore.FieldValue.arrayRemove(data.userToUnsubscribe.uid), 'nbRemainingPlaces': meal['nbRemainingPlaces'] + 1, 'pendingRequest': pendingRequest })
          promises.push(p4);
          if (promises.length > 0) {
            return Promise.all(promises)
              .then(() => console.log('user unsubscribed'))
              .catch((err: any) => console.log('error unsubscribing user : ' + data.userToUnsubscribe.first_name + ' ' + data.userToUnsubscribe.last_name + ' of the meal ' + meal.uid + '. Err : ' + err))
          }
          else {
            // doc.data() will be undefined in this case
            throw new functions.https.HttpsError('not-found', 'no such document!');
          }
        } else {
          // doc.data() will be undefined in this case
          console.log('doc does not exist');
          throw new functions.https.HttpsError('not-found', 'no such document!');
        }
      })
  })
});

Triggering unsubscribeToMeal function goes from 60MB memory usage without the ssr function deployed to 240MB memory usage with.

So I am wondering what is happening ? It looks like the express server app is bootstrap on each cloud function instance which causes an increase of memory use and so more billing.

I limited global variables to minimze cold start as Doug Stevenson explains here, so it shouldn't be that.

server.ts

(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xhr2');

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
export const app = express();
import * as cookieParser from 'cookie-parser';
import { from } from 'rxjs';
app.use(cookieParser());

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');


// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req: express.Request, res: express.Response) => {
  res.render('index2', {
    req
  });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});

Any solution to still have low memory usage (without increasing memory limit of the function) and at the same time ssr function with express server for angular universal ?

3

3 Answers

0
votes

I my opinion it the increase of the memory is nothing unexpected. There is description in your tutorial:

Angular is a client-side framework designed to run apps in the browser. Universal is a tool that can run your Angular app on the server,...

So using SSR solution you are moving running angular app form client to server, so all the code run on normally on client side, now is run on the server. I don't think that it will be possible to run something without reserving resources needed to do it...

Maybe in this particular case AppEngine will be better to deploy it... I hope it will help you somehow :)

0
votes

After getting in touch with the firebase community: "using firebase deploy --only functions will redeploy the code for all your functions and the memory used will be cumulative total of all those functions loaded together". So the solution is to split functions into different files and use process.env.[functionName] with a if statement as explained here to avoid this increase of memory for the expressFunction.

0
votes

Yes vitooh ! You need to split each function into a ts or js file. Here is my new structure:

  • /functions
    • /src
      • index.ts
      • /my_functs
        • ssr.ts
        • unsubscribeToMeal.ts
        • ... other functions

Each function file need to start with exports = module.exports = functions... For instance, here is the code for my new express function (you can compare to what I had in my first post)

functions/src/my_functs/ssr.ts file
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
import * as functions from 'firebase-functions';

exports = module.exports = functions.https.onRequest(require(`${process.cwd()}/dist/server`).app);

Don't forget to import in each file what is needed such as "import * as functions from 'firebase-functions';". I also changed my index.ts file which is used when deploying to firebase with : firebase deploy --only functions and installed glob (npm i glob) from the functions folder.

functions/src/index.ts file

const glob = require("glob");
if (process.env.FUNCTION_NAME) {
  // Specific function requested, find and export only that function
  const functionName = process.env.FUNCTION_NAME;
  const files = glob.sync(`./my_functs/${functionName}.@(js|ts)`, { cwd: __dirname, ignore: './node_modules/**' });
  const file = files[0];
  exports[functionName] = require(file);
} else {
  // Name not specified, export all functions
  const files = glob.sync('./my_functs/*.@(js|ts)', { cwd: __dirname, ignore: './node_modules/**' });
  for (let f = 0, fl = files.length; f < fl; f++) {
    const file = files[f];
    const functionName = file.split('/').pop().slice(0, -3); // Strip off '.(js|ts)'
    exports[functionName] = require(file);
  }
}