node-device-detector icon indicating copy to clipboard operation
node-device-detector copied to clipboard

Very slow + cpu intense

Open MickL opened this issue 2 years ago • 10 comments

The detection is very slow and cpu intense. For me it is not usable in production. The following I tested on a Macbook 16 M1 Pro with just the sample from the readme:

PostmanRuntime/7.29.0" -> 310ms

"Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36" -> 86ms

Code:

const DeviceDetector = require('node-device-detector');
const detector = new DeviceDetector;
const userAgent = 'Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36';

const t0 = Date.now();
detector.detect(userAgent);
const t1 = Date.now();
console.log(`Took '${t1-t0}ms'`)

MickL avatar Jun 20 '22 13:06 MickL

try to use

const detector = new DeviceDetector({deviceIndexes: true});

This will increase memory consumption, but reduce the time for known devices, this will also reduce the CPU load;

  • use a cache for production

const createHash = (userAgent) =>  {
    return crypto.createHash('md5').update(String(userAgent)).digest('hex');
}
const detect => async (userAgent) => {
    let result;
    let uaKey = 'UA:' + createHash(userAgent);

    let cacheResult = await cache.get(uaKey);
    if (cacheResult === void 0) {
      result = deviceDetector.detect(userAgent);
      await cache.set(uaKey, JSON.stringify(result), cache.TIME_15_MINUTE);
      return result;
    } 
   return JSON.parse(cacheResult);
}

cache adaper for memcached

const Memcached = require('memcached');

//Memcached.config.debug = true;

class Memcache {
  /** @typedef {Memcached} */
  #cache = null;

  static EVENT_FAILURE = 'failure';
  static EVENT_RECONNECTING = 'reconnecting';

  constructor() {
    this.TIME_MAX = 2592000;
    this.TIME_DAY = 86400;
    this.TIME_HOUR = 3600;
    this.TIME_15_MINUTE = 900;

    this.adapter = new Memcached(['127.0.0.1:11211']);
  }

  set adapter(adapter) {
    this.#cache = adapter;
  }

  get adapter() {
    return this.#cache;
  }

  /**
   * @param {String} key
   * @param {*} val
   * @param {Number} expTime
   * @returns {Promise<any>}
   */
  set(key, val, expTime = 3600) {
    return new Promise((resolve, reject) => {
      this.adapter.set(key, val, expTime, (err) => {
        if (err) {
          reject(err);
        } else {
          resolve(null);
        }
      });
    });
  }

  /**
   * @param {String} key
   * @returns {Promise<any>}
   */
  remove(key) {
    return new Promise((resolve, reject) => {
      this.adapter.del(key, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

  /**
   * @param {String} key
   * @returns {Promise<any>}
   */
  get(key) {
    return new Promise((resolve, reject) => {
      this.adapter.get(key, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

  on(event, callback) {
    this.adapter.on(event, callback);
    return this;
  }

  end() {
    this.adapter.end();
  }
}

module.exports = Memcache;

sanchezzzhak avatar Jun 20 '22 15:06 sanchezzzhak

Using deviceIndexes: true does not change anything for me. The string Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36 always is about 84-86ms

MickL avatar Jun 21 '22 11:06 MickL

try this example

const detector = new DeviceDetector({deviceIndexes: true});

let lastCpuUsage;
const cpuUsage = () => {
  return lastCpuUsage = process.cpuUsage(lastCpuUsage);
}

const createTest = (testname, ua) => {
  console.time(testname);
  cpuUsage()
  detector.detect(ua);
  console.timeEnd(testname);
  console.log(testname, ua);
  console.log(testname, cpuUsage())
  console.log('------')
}

const userAgent1 = 'Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36';
const userAgent2 = 'Mozilla/5.0 (Linux; Android 8.0.0; ATU-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36';
const userAgent3 = 'Mozilla/5.0 (Linux; Android 4.2.2; Trooper_X40 Build/Trooper_X40) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36';
const userAgent4 = 'Mozilla/5.0 (Linux; U; Android 4.2.2; zh-CN; R831K Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/10.3.1.549 U3/0.8.0 Mobile Safari/534.30';

createTest('test1', userAgent1);
createTest('test2', userAgent1);
createTest('test3', userAgent2);
createTest('test4', userAgent3);
createTest('test5', userAgent4);
createTest('test6', userAgent1);

my result:

test1: 185.317ms
test1 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test1 { user: 215636, system: 444 }
------
test2: 79.819ms
test2 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test2 { user: 287318, system: 8210 }
------
test3: 63.22ms
test3 Mozilla/5.0 (Linux; Android 8.0.0; ATU-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36
test3 { user: 350291, system: 8281 }
------
test4: 3.024ms
test4 Mozilla/5.0 (Linux; Android 4.2.2; Trooper_X40 Build/Trooper_X40) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36
test4 { user: 353597, system: 8281 }
------
test5: 27.372ms
test5 Mozilla/5.0 (Linux; U; Android 4.2.2; zh-CN; R831K Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/10.3.1.549 U3/0.8.0 Mobile Safari/534.30
test5 { user: 381758, system: 8281 }
------
test6: 2.011ms
test6 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test6 { user: 383923, system: 8281 }
------


the first launch is very long, and it takes time for V8 to optimize the code, create a'test web server and periodically send requests.

offtop: I plan to make optimizations for the search of a web browser + desktop device (in process)

sanchezzzhak avatar Jun 21 '22 12:06 sanchezzzhak

preliminary optimizations to identify the client gave an increase of 38-50% :arrow_up:

Old tests

test1: 72.8ms
test1 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test1 { user: 510964, system: 19953 }
------
test2: 47.126ms
test2 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test2 { user: 550069, system: 27775 }
------
test3: 64.484ms
test3 Mozilla/5.0 (Linux; Android 8.0.0; ATU-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36
test3 { user: 612832, system: 27775 }
------
test4: 2.503ms
test4 Mozilla/5.0 (Linux; Android 4.2.2; Trooper_X40 Build/Trooper_X40) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36
test4 { user: 615422, system: 27775 }
------
test5: 26.792ms
test5 Mozilla/5.0 (Linux; U; Android 4.2.2; zh-CN; R831K Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/10.3.1.549 U3/0.8.0 Mobile Safari/534.30
test5 { user: 642282, system: 27831 }
------
test6: 1.473ms
test6 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test6 { user: 643962, system: 27831 }
------
test7: 463.938ms
test7 Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30
test7 { user: 1108373, system: 27958 }
------
test8: 4.503ms
test8 Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
test8 { user: 1114473, system: 32131 }
------
test9: 239.04ms
test9 Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30
test9 { user: 1345244, system: 40100 }
------
test10: 5.54ms
test10 Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30
test10 { user: 1350913, system: 40100 }
------

banchmarks.js

Test: Mozilla/5.0 (Linux; Android 5.0; NX505J Build/KVT49L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.78 Mobile Safari/537.36
# detect only device
device DisableIndexes x 236 ops/sec ±90.66% (87 runs sampled)
device EnableIndexes x 10,727 ops/sec ±0.37% (93 runs sampled)

# detect only client
client EnableIndexes x 1,545 ops/sec ±1.04% (89 runs sampled)
client DisableIndexes x 718 ops/sec ±1.60% (85 runs sampled)

sanchezzzhak avatar Jun 22 '22 12:06 sanchezzzhak

Hi, I'm trying out the library in a lambda environment and I see typical detection taking around 600 ms, with some going upwards of 3-4 seconds (I'm using time/timeEnd around the detection code) to identify os, client, device, and bot. I'm not using clientHints. Would that help speed this up some? Any other ideas? It's a lambda with 384M of RAM. This is on version 2.0.4.

petehanson avatar Jun 29 '22 17:06 petehanson

hi, show me the code how you use the library.

sanchezzzhak avatar Jun 29 '22 17:06 sanchezzzhak

here's the function I'm running:

decorateWithUserAgentData(userAgent) {

    const returnData = {
        os: null,
        client: null,
        device: null,
        bot: null
    };

    if (typeof userAgent === "string") {

        const detector = new DeviceDetector({
            clientIndexes: true,
            deviceIndexes: true,
            deviceAliasCode: false,
        });

        const result = detector.detect(userAgent);
        const bot = detector.parseBot(userAgent);

        if ('os' in result && Object.keys(result['os']).length > 0) {
            returnData['os'] = result['os'];
        }

        if ('client' in result && Object.keys(result['client']).length > 0) {
            returnData['client'] = result['client'];
        }

        if ('device' in result && Object.keys(result['device']).length > 0 && _.get(result, 'device.id', '') !== '') {
            returnData['device'] = result['device'];
        }

        if (Object.keys(bot).length > 0) {
            returnData['bot'] = bot
        }

    }

    return returnData;
}

Thanks for the help and taking a look at this! It's a method that adds elements to a bunch of other clickstream data.

petehanson avatar Jun 29 '22 18:06 petehanson

I did try a version of the cache approach above like so:

decorateWithUserAgentData(userAgent) {

    const returnData = {
        os: null,
        client: null,
        device: null,
        bot: null
    };

    if (typeof userAgent === "string") {

        const key = StringUtils.md5(userAgent);

        if (key in map) {
            const record = map[key];
            return record;
        } 

        const detector = new DeviceDetector({
            clientIndexes: true,
            deviceIndexes: true,
            deviceAliasCode: false,
        });

        const result = detector.detect(userAgent);
        const bot = detector.parseBot(userAgent);

        if ('os' in result && Object.keys(result['os']).length > 0) {
            returnData['os'] = result['os'];
        }

        if ('client' in result && Object.keys(result['client']).length > 0) {
            returnData['client'] = result['client'];
        }

        if ('device' in result && Object.keys(result['device']).length > 0 && _.get(result, 'device.id', '') !== '') {
            returnData['device'] = result['device'];
        }

        if (Object.keys(bot).length > 0) {
            returnData['bot'] = bot
        }

    }

    return returnData;
}

I pulled a couple of weeks of user agent data and then counted each one and generated a hash of results from each string that covers 95% of agents. I did decrease execution time quite a bit, but it's not a perfect solution to this. Though the slight speed penalty on 5% of the traffic may be ok here.

petehanson avatar Jun 29 '22 19:06 petehanson

https://docs.aws.amazon.com/lambda/latest/operatorguide/global-scope.html I have no idea how lambda works and what happens after the function is executed with objects(

Object creation new DeviceDetector must be called once. The fact is that when we create an object, we load data from disk every time.

1

const detector = new DeviceDetector({
            clientIndexes: true,
            deviceIndexes: true,
            deviceAliasCode: false,
        });    
   
decorateWithUserAgentData(userAgent) {

    // ...
}

2

let detector;
decorateWithUserAgentData(userAgent) {
   if (detector === void 0) {
        detector  = new DeviceDetector({
            clientIndexes: true,
            deviceIndexes: true,
            deviceAliasCode: false,
        });
    }
    // ...
}

3

decorateWithUserAgentData(userAgent) {
   if (this.detector === void 0) {
        this.detector = new DeviceDetector({
            clientIndexes: true,
            deviceIndexes: true,
            deviceAliasCode: false,
        });
    }
    // ...
}

try one of the three examples

sanchezzzhak avatar Jun 29 '22 20:06 sanchezzzhak

Ah, I see, so the constructor has a high cost to initially run. Yeah, if you did the initialization in the global scope of a lambda, that would be available for subsequent invocations while the function remains warm. The downside there is that each time a new concurrent request happens and lambda cold starts another instance of the function, it would be a new invocation of the constructor.

I can try moving the constructor to the global space and see how that works. My work load is somewhat predictable, so cold starts aren't too much of an issue.

Thanks for taking a look at this!

petehanson avatar Jun 29 '22 20:06 petehanson