Reducing Network Traffic with the Flyweight Design Pattern

WBC flyweight champion Yu Kimura
Image by: Papakanno (CC BY-SA)

Your users expect a fast and smooth experience. Sometimes what stands in the way is how long it takes to download data. The flyweight design pattern is a possible solution to help reduce bandwidth in your app.

Data is a precious thing. Every organization wants as much as it can get a hold of. Collecting and using the data can be harmful for user experience or even breach a company’s outbound/inbound traffic quota policy.

The Use Case

You wish to collect data from your end users. Your app has this amazing analytics feature. It periodically sends metrics you can later analyze and build your business upon. The more data you collect, the more power you have in your analysis.

This process, when scaled, can become problematic. You add more and more metrics and the messages get bigger and bigger. Eventually, the end user (or his IT team) notice the outbound bandwidth spikes. You get a ticket you need to solve – please reduce the bandwidth signature of your app, or we cancel the deal.

It can also be problematic to your application. Your server gets a lot of messages in a very short time. If these messages are growing in size, you might have memory issues storing and handling all this data. This might lead to noticeable performance issues or even service availability issues.

The other side of the coin is when you want to send data to the end-user.

Imagine that you are tracking an application’s state in one machine (e.g. a multiplayer game or a document with multiple people editing several parts of it).

The system needs to sync between the states of all of the users in real time. Hence, your end users send real-time updates to the server. The server, in turn, pushes the data back to the other clients for update (via socket or other async communication protocol).

This way we have high speed updates (multiple clients sending periodical states). In addition, we add more features so the messages get bigger. Eventually, if the need for scale arises, you’ll see bandwidth spikes due to massive data collection.

Shall We Play a Game?

The gaming industry has already learned how to handle bandwidth issues. It’s not the bandwidth the usual web developer would think of. Let’s see why game builders use the flyweight design patterns.

Games use the GPU (Graphical Processing Unit or Graphics Card) heavily. A lot of data is being sent to be processed in the GPU.

The CPU is usually very fast while dealing with one problem (we can have multiple cores, but they are also limited to 2/4/8/16 etc.). The GPU, on the other hand, has a “secret power”. It’s “secret power” is parallelism.

When tasked with one task, it performs slower than the CPU. When dealing with thousands of computation tasks at the same time – it can finish tasks that the serial CPU would take much longer to accomplish.

In any event, the problem lies in getting data into the processing unit. The bigger the data, the bigger our problem.

While the processing unit is waiting for the data to come in, it sits idle. Even worse – it can stop in the middle of a computation, while part of the data it needs to complete its tasks is stuck in the heavy traffic.

Figure 1: Left to right – data is waiting to be processed by the CPU. The CPU sends chunks to be processed by the GPU. If the CPU is slow – the GPU sits idle. If the GPU can’t take all the data – your game will have to wait for the response. Even if the CPU knows you hit the bad guy – the visual effect will be stalled by the GPU.
But GPU is not only graphics… back in the day, we used CUDA to do parallel computing from Matlab using the GPU.
Nostalgia…

The Flyweight Design Pattern

So… we have lots of data that’s passing into our clients/server/CPU/GPU. Some part of our system gets clogged and is our bottleneck. It’s bogged down under the data pressure.

What can we do about it?

The flyweight design pattern is a very simple solution that helps you make your data thinner.

In essence – your data has static meta data in it. For instance, in an analytics type of data, you can have the browser, OS, user agent etc. This kind of data repeats itself over a lot of messages.

The flyweight pattern suggests that you extract the static data from every instance. Then, send the “thin” data with only one instance of the static data. The receiver should have some mechanism to know how to piece the data back together.

Sounds abstract? Let’s look at an example.

Example

The use cases mentioned above are a bit complex. In order to show how the pattern is used, I’ve created a simple example.

// grab the packages we need
const express = require('express');
const app = express();
const faker = require('faker');
const port = process.env.PORT || 3000;
const nResponses = 1000;
const TYPES = {
WORKER: 'WORKER',
COP: 'COP',
TEACHER: 'TEACHER'
};
TYPES.getRandom = function() {
const keys = Object.keys(this);
return this[keys[Math.floor(Math.random() * (keys.length - 1))]];
}
const TYPE_TOOLS = {
WORKER: ['hammer', 'nail', 'ladder', 'helmet', 'screwdriver', 'swiss knife', 'boots'],
COP: ['uniform', 'taser', 'pistol', 'police hat', 'badge', 'whistle'],
TEACHER: ['marker', 'book', 'notebook', 'tablet', 'papers', 'tie']
};
class UserMetaData {
constructor(type) {
this.type = type;
this.tools = TYPE_TOOLS[type];
}
}
class User {
constructor(index, type) {
this.id = `${index}-${new Date().getTime()}`;
this.name = faker.name.firstName() + ' ' +faker.name.lastName();
this.email = faker.internet.email();
this.metaData = new UserMetaData(type);
}
}
class FlyWeightUser {
constructor(index, type) {
this.id = `${index}-${new Date().getTime()}`;
this.name = faker.name.firstName() + ' ' +faker.name.lastName();
this.email = faker.internet.email();
this.type = type;
}
}
function generateData(userClass) {
let users = [];
for (let i = 0; i < nResponses; i++) {
users.push(new userClass(i, TYPES.getRandom()));
}
return users;
}
// routes will go here
app.post('/getData', function(req, res) {
res.send(generateData(User));
});
app.post('/getFlyWeightData', function(req, res) {
res.send({data: generateData(FlyWeightUser), TYPE_TOOLS});
});
app.use(express.static('dist'));
// start the server
app.listen(port);
console.log('Server started! At http://localhost:' + port);
view raw simpleServer.js hosted with ❤ by GitHub

Code Snippet 1: Simple code that emulates a server. The server exposes two end points (getData and getFlyweightData) that returns a data structure with nResponses of entities.

In code snippet 1, the server sends the data to the client in two ways.

The first and naive way is by using the User class (line 28). In its constructor, we add the UserMetaData directly on the user (line 34).

Every time we send the data to the client (or to another service) – the recipient gets an array of tools for each element in the data array.

The other end point uses the FlyWeightUser class (line 38). This class saves only the user type(line 44).

Another part of the pattern is to also send the static meta data itself (also called intrinsic state by the Gang of Four).

Comparing the getData end point (line 59) to the getFlyWieghtData end point (line 63) we can see a difference in the data structure.

The getData entry point just sends the array of entities. The getFlyWeightData entry point sends an object containing the array of FlyWeightUser instances. In addition, it sends the meta data dictionary (line 64) .

This way, we send the static (intrinsic) meta data only once and not for every entity in our array.

The recipient of the data has all the clues it needs to match the type with the meta data.

Results

While this example is very simple, it still shows the benefit.

Let’s run a simple client:

(function() {
function json(response) {
return response.json()
}
function getData(url) {
fetch(url, {
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
}
})
.then(json)
.then(function (data) {
myUsers = data;
console.log('Finished fetching data');
})
}
function regularData() {
getData('/getData')
}
function flyWeightData() {
getData('/getFlyWeightData');
}
let myUsers;
document.getElementById('request').addEventListener('click', regularData);
document.getElementById('requestFlyWieght').addEventListener('click', flyWeightData);
})();
view raw flyWeight.js hosted with ❤ by GitHub
<button id="request">Request data</button>
<button id="requestFlyWieght">Request light weight data</button>
<script src="flyWeight.js"></script>
view raw index.html hosted with ❤ by GitHub

Code Snippet 2: A client that uses the two endpoints.

The client in code snippet 2 is very simple as well. It has 2 buttons – one fetches the data using the getData endpoint and the other using the getFlyWeightData end point.

The results are shown in Figure 2. The getFlyWeightData message size is around 14% smaller.

Put this at scale – your analytics messages sent every 2 seconds or your game engine sends an update every frame – and you get a huge reduction in traffic.

Figure 2: getFlyWeightData vs getData. The data transferred from server to client was reduced by 10%.

In our case, lets look at a user that visits your app for an hour. 3600 seconds / 2 messages/second = 1800 messages every hour. This sums up to 96.3KB * 1800. With the Fly Weight algorithm, we saved 25MB.

With real life data the reduction can be even more significant.

Another point to think about is users with slower internet connection – like G3 or slower.

You can actually test it on your own using chrome dev tools throttling. See Figure 3, in which the time it takes a 3G connection to fetch the data is shown.

Figure 3: The same test over a slow 3G connection (emulated). The difference in this simple example is 240 milliseconds. These are 240 milliseconds that can be saved for your end user.

Summary

The Flyweight design pattern is heavily used in games. It can help save some memory (we have less duplicates in our precious memory). But that’s not its primary use case, since most of our duplicates will just be references to an address in memory.

Its more critical use is saving bandwidth. In gaming – mostly to the processing units. In your application – the data sent back and forth between your services.

We described some use cases. It can help you reduce the load on your services. It can also help you improve user experience – especially for users with a slow internet connection.

It can also save you from an angry client complaining on your SDK breaching their outbound traffic policy (personal experience).

At WalkMe, we’ve used this pattern in various use cases. One of them was for our analytics feature. Our client-side events emitter sent huge amount of data to our events collector service.

Some of our clients monitored the traffic and saw a significant increase in outbound traffic since installing our SDK. Using the flyweight pattern, we managed to reduce the amount of outbound traffic for all of our customers.

In the example shown in this article, we’ve used the Flyweight pattern in order to save on web traffic bandwidth. Note that this example as well as the results are very just to show there is an effect.

The more complex your data and the slower your recipient’s connection – the more critical the difference.

Thanks!

Thanks for this article reviewers Andy Van Slaars and MichalKutz

Sign up to my newsletter to enjoy more content:

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments