Code splitting แบบใหม่ใน Webpack 4

สำหรับคนใช้ Webpack ผมเชื่อว่าหลายๆ คนจะใช้ code splitting อยู่ และ code splitting ที่ง่ายๆ โง่ๆ ตรงไปตรงมาที่สุดที่น่าจะใช้กันก็คือ vendor code split หรือการแยกโค้ดใน node_modules ไปเป็นอีก bundle เนื่องจากว่าโค้ดในนั้นมักจะอัพเดตไม่บ่อยเท่าโค้ดเรา ก็จะช่วยให้แคชได้นานขึ้น

แต่พอถึงยุค Webpack 4 แล้ว เราสามารถทำ Vendor code split ได้มากขึ้น และเผลอๆ จะ split ได้ยิบย่อยลงไปได้อีกด้วยระบบ Chunk graph

ปัญหาของ code split

ก่อนจะไปถึง vendor code split นั้นจะขอเล่าถึง Code split แบบใหม่ใน Webpack 4 ก่อน

ใน Webpack 3 นั้นหากเราทำ async code splitting นั้นเราจะได้ Bundle ประมาณนี้

จากรูปจะเห็นปัญหาว่า หาก entrypoint ไม่ได้ใช้ lodash แต่ทั้ง Overlay และ Settings chunk ใช้ lodash จะทำให้ Webpack เอา lodash ไปใส่ทั้ง 2 bundle user ก็ต้องโหลด lodash หลายรอบ

วิธีแก้ไขในอดีตก็มักจะใช้ vendor code split เพื่อแยก node_modules ออกไป (หรือ CommonsChunkPlugin ที่แยกตามจำนวนครั้งที่ใช้)

หรือถ้าโกงหน่อยก็จะใส่ require('lodash') ไว้ใน entrypoint เพื่อบังคับให้ entrypoint มีการเรียก lodash

แต่ปัญหาก็ยังไม่จบแค่นั้น เพราะพอเราแยก node_modules ออกไปก็จะได้ chunk ใหม่ที่มีทุกมอดูลรวมกันอยู่ ก็กลายเป็นว่าพอเข้าหน้า Overlay ก็ต้องโหลด jQuery มาด้วยถึงจะไม่ได้ใช้ (ในรูปไม่ได้วาดไว้ แต่สมมุติว่ามีหน้าที่ใช้ jQuery หลายหน้า)

ในเว็บ TipMe ก็มีปัญหาแบบนี้เช่นเดียวกันเพราะเว็บเราใช้ jQuery แต่ก็จะมีบาง widget ที่ใช้ React เขียน ก็คงไม่เหมาะที่จะต้องโหลด React ทุกหน้า วิธีที่เราทำอยู่คือทำ DLL bundle 3 ชุด

  • jQuery
  • React
  • Runtime (common ระหว่าง 2 bundle ด้านบน เช่น Polyfill)

เสร็จแล้วเวลา build เราจะมี Gulp filter ที่อ่านไฟล์แล้วดูว่าไฟล์นี้มีคำสั่ง // use React หรือเปล่า ถ้ามีก็จะ link กับ jQuery + React DLL แต่ถ้าไม่มีก็จะ link กับ jQuery อย่างเดียว

วิธีนี้ก็ยังไม่ใช่วิธีที่ดีเท่าไร เพราะ

  • DLL ก็ยังมีมอดูลที่ไม่จำเป็นอยู่
  • ต้องนั่ง list module ที่ใช้ DLL เอง ซึ่งไม่รู้ว่าเราเลือกมาแล้วดีที่สุดหรือเปล่า
  • DLL ทำ Tree shaking กับโค้ดเราไม่ได้ เพราะเวลา build DLL มันไม่อ่านโค้ดเรา
  • Build tooling ซับซ้อน
  • เวลา build ก็ซับซ้อน ต้องรัน Gulp 3 รอบ คือ build runtime, build DLL แล้วถึงจะ build app
  • เวลาใช้ก็ซับซ้อน เพราะที่หน้าต้อง include DLL ให้ถูกว่าหน้านี้ใช้อะไร

All new code splitting

Webpack 4 นั้นจะเปลี่ยนวิธี split ใหม่โดยค่าเริ่มต้นนั้นจะตัด vendor chunk ได้ฉลาดมากขึ้น

นั่นคือ Webpack สามารถสร้าง chunk ที่เป็น common ระหว่างมอดูลได้แล้ว โดยเป็นค่า default เราไม่ต้องทำอะไรเพียงแค่ใช้ Webpack 4 เท่านั้น

แต่จะเห็นว่าชื่อไฟล์นั้นจะเป็น vendor~page-a~page-b ซึ่งพอเราเพิ่มหน้าเข้าไปไฟล์ก็จะเปลี่ยนชื่อ ถ้าต้องการ cache ให้ได้นานๆ นั้นแนะนำให้ตั้งค่า Webpack ดังนี้

{
    optimization: {
        splitChunks: {
            name: false,
        }
    }
}

ซึ่งจะทำให้ chunk ที่ได้นั้นใช้ชื่อเป็น chunk ID แทนที่จะระบุชื่อมอดูลภายใน

Entrypoint splitting

ถึงตรงนี้แล้ว splitting แบบใหม่ก็ยังไม่ดีเท่าของเก่า เพราะมันยังไม่แยก node_modules ออกจาก Entrypoint (หรือถ้ามีหลาย entrypoint มันก็ยังไม่แยก common ออกมา) เพราะค่าเริ่มต้นของ Webpack จะปิดเอาไว้

เหตุผลที่ปิดก็เดาได้ไม่ยากครับ ปกติถ้าเรา split มือ เราจะรู้ชื่อ chunk แล้วเราก็จะเขียน script tag ไปเรียกมา แต่ถ้า Webpack ทำให้ user ก็อาจจะไม่เข้าใจว่าต้องเรียก chunk ที่แยกออกมา ก็เลยปิดไว้เป็นหลัก

ทีนี้เราจะเปิดได้อย่างไร? วิธีการเปิดคือตั้งค่าดังนี้ใน Webpack

{
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 5, // default to 2, adjust as needed
        },
        runtimeChunk: 'single', // optional, recommended
    }
}

ซึ่งค่าต่างๆ จะมีผลดังนี้

  • maxInitialRequests จะกำหนดว่าให้ split ได้สูงสุดกี่ chunk
  • runtimeChunk จะแยกส่วน manifest ไปเป็นอีก chunk ซึ่ง manifest จะเปลี่ยนทุกครั้งที่มีการแก้โค้ดไม่ว่าไฟล์ไหนอยู่แล้ว การแยกไปเลยก็จะทำให้ cache ได้ดีขึ้น
  • อื่นๆ สามารถดูได้ตาม docs

จากนั้นเราจะต้องแก้หน้าเว็บให้ใส่ script tag ให้ครบ แต่… ในเมื่อ Webpack generate ชื่อไฟล์มาให้ แล้วเราจะใส่ src ยังไง?

คำตอบคือในไฟล์ Webpack stats จะมีบอกอยู่ว่า Entrypoint นี้ต้องใช้ไฟล์อะไรบ้าง หน้าตาประมาณนี้

{
  "entrypoints": {
    "bootstrap": {
      "chunks": [
        28,
        10,
        11,
        49
      ],
      "assets": [
        "js/runtime~bootstrap.js",
        "js/runtime~bootstrap.js.map",
        "js/c_10.43ba1082.js",
        "js/10.js.map",
        "js/c_11.45f9671c.js",
        "js/11.js.map",
        "js/c_bootstrap.5f882904.js",
        "js/bootstrap.js.map"
      ]
    },
  }
}

(ถ้าทำ CSS Split ด้วย ก็จะได้ชื่อไฟล์ CSS มาใน assets ด้วยเช่นกัน)

สิ่งที่เราต้องทำคือ อ่านไฟล์ stats แล้วเอามาสร้าง HTML

ก่อนอื่นก็แก้ Webpack ให้สร้าง stats มาเสียก่อน โดยใช้ webpack-stats-plugin

{
    plugins: [
        new stats.StatsWriterPlugin({
            filename: 'stats.json',
            fields: ['entrypoints'],
        }),
    ],
}

จากนั้นเวลา build เราจะได้ไฟล์ stats.json ออกมา จากนั้นเราก็จะแก้ให้แอพเราอ่านไฟล์นี้ขึ้นมา

สำหรับ Django นั้นวิธีที่ผมใช้อยู่ก็คือสร้าง Template tag ขึ้นมา

โดยนำไฟล์ webpack.py ไว้ใน folder templatetags และไฟล์ webpack_entrypoint.html ไว้ใน templates จากนั้นเวลาใช้ก็

{% load webpack %}
{% entrypoint "bootstrap" %}

ก็จะได้ script tag ที่ครบถ้วนทันที

<script src="https://static.tipme.in.th/js/runtime~bootstrap.24e2bfeb3c73.js" crossorigin="anonymous"></script>
<script src="https://static.tipme.in.th/js/c_10.43ba1082.2b93f172c643.js" crossorigin="anonymous"></script>
<script src="https://static.tipme.in.th/js/c_11.45f9671c.d9617c7f9295.js" crossorigin="anonymous"></script>
<script src="https://static.tipme.in.th/js/c_bootstrap.5f882904.9c15205b28f3.js" crossorigin="anonymous"></script>

สรุป

Code split แบบใหม่นี้ลดความซับซ้อนไปในหลายๆ อย่างเลยครับ ในขณะที่เพิ่ม performance ให้ดีขึ้น และยิ่งถ้าเปิดให้มัน split entrypoint ได้ก็ยิ่งเพิ่มประสิทธิภาพอีก

Commit ที่ TipMe ปรับมาใช้ Webpack 4 นั้น สถิติคือ 144 additions, 324 deletions ลบ tooling ออกไปได้เยอะมาก

  • ไม่ต้อง mark แล้วว่า entrypoint นี้ใช้ React เพราะ Webpack จะรู้เองแล้วเอา React chunk ใส่มาใน list ให้อัตโนมัติ
  • ไม่ต้องมี webpack config หลายชุดแล้ว build ทีเดียวออกเลย
  • Build target เหลืออันเดียว
  • ใช้ mode: production แทนการเซตเอง

และสุดท้าย Webpack build time จาก 62550ms เหลือ 35162ms หรือลดลง 43%

(แต่ Gulp เราลดมาแค่ 20 วินาที เพราะมีไฟล์ที่ต้องอัพโหลดขึ้น Sentry มากขึ้น)

ทำไมเน็ตช้า! หาคำตอบด้วย M5Stack และ Raspberry Pi

หลายสัปดาห์ก่อนเพิ่ง Download Dota 2 กลับมาครับ แล้วก็พบว่าเวลามัน patch เน็ตมันช้ามาก แถมรันเป็น background ไม่รู้อีกว่าเน็ตโดนสูบอยู่ นึกว่าคอมมีปัญหาเพราะไม่มีใครใช้เน็ตแล้วมันจะช้าได้ยังไง

ถ้าใช้ router แพงๆ มันมักจะมีจอมาด้วยซึ่งจะแสดง network speed อยู่ ก็เลยคิดว่าจะลองเขียนอะไรแบบนั้นดู

แผนการ

สำหรับแผนการในรอบนี้คือผมจะเอา M5Stack อ่าน SNMP จาก router ผม แล้วมาแสดงบนหน้าจอ

M5Stack

espressif.com

พอดีไปเดินงาน Maker Faire Bangkok ก็เลยยุให้พ่อซื้อ M5Stack ให้ตัวหนึ่ง (บังเอิญเจอพี่ลิ่ว Blognone ด้วย เลยให้พี่ลิ่วช่วยยุ) ซึ่งผมว่ามันเจ๋งมากเพราะมันมีจอ ปุ่มกด และ battery ในตัว (แบตเท่าที่ลองอยู่ได้ประมาณ 10 นาที อย่าคาดหวังอะไรมันมาก และมันจะ reset ตอนเสียบไฟ)

สำหรับการเขียนโปรแกรมนั้น M5Stack ใช้ CPU ESP32 (ตัวพี่ของ ESP8266 ยอดนิยม) สามารถใช้ Arduino IDE เขียนได้ ซึ่งปรากฏว่า Arduino ไม่มี SNMP client library ครั้นจะเขียนเองก็ยุ่งยากไป เลยคิดว่าจะเอา Raspberry Pi มาอ่านแล้วส่งข้อมูลให้ M5Stack อีกที

Router configuration

ก่อนจะไปถึง Raspberry Pi มาตั้งค่า router ให้มีข้อมูลพร้อมอ่านกันก่อน สำหรับ Router Mikrotik ทำได้ดังนี้

  1. เปิด WebFig/WinBox ขึ้นมาแล้ว login ให้เรียบร้อย
  2. ไปที่เมนู IP > SNMP
  3. เลือก Enabled
  4. คลิกที่ Communities ด้านบน กด Add New
  5. ระบุข้อมูลดังนี้
    • Name: อะไรก็ได้
    • Addresses: ไม่ใส่ก็ได้ หรือถ้าต้องการให้ปลอดภัยระบุเป็น IP ของ Raspberry Pi หรือ CIDR ก็ได้
    • Read Access: ติ๊กไว้
    • ผมยังไม่เปิดใช้ security เนื่องจากเป็น read only และ allow เฉพาะ LAN IP ไว้เลยคิดว่าความเสี่ยงต่ำพอแล้ว
  6. ตอนนี้น่าจะพร้อมใช้งาน SNMP แล้ว

ถัดมาเราจะหา OID ของ interface ที่ออกเน็ต ขั้นตอนนี้จะต้องใช้เมนู Terminal แล้วใช้คำสั่ง /interface print oid จะได้ผลดังนี้

 0  R  name=.1.3.6.1.2.1.2.2.1.2.1 actual-mtu=.1.3.6.1.2.1.2.2.1.4.1 mac-address=.1.3.6.1.2.1.2.2.1.6.1 admin-status=.1.3.6.1.2.1.2.2.1.7.1 oper-status=.1.3.6.1.2.1.2.2.1.8.1 bytes-in=.1.3.6.1.2.1.31.1.1.1.6.1 packets-in=.1.3.6.1.2.1.31.1.1.1.7.1 discards-in=.1.3.6.1.2.1.2.2.1.13.1 
       errors-in=.1.3.6.1.2.1.2.2.1.14.1 bytes-out=.1.3.6.1.2.1.31.1.1.1.10.1 packets-out=.1.3.6.1.2.1.31.1.1.1.11.1 discards-out=.1.3.6.1.2.1.2.2.1.19.1 errors-out=.1.3.6.1.2.1.2.2.1.20.1 

 1  RS name=.1.3.6.1.2.1.2.2.1.2.2 actual-mtu=.1.3.6.1.2.1.2.2.1.4.2 mac-address=.1.3.6.1.2.1.2.2.1.6.2 admin-status=.1.3.6.1.2.1.2.2.1.7.2 oper-status=.1.3.6.1.2.1.2.2.1.8.2 bytes-in=.1.3.6.1.2.1.31.1.1.1.6.2 packets-in=.1.3.6.1.2.1.31.1.1.1.7.2 discards-in=.1.3.6.1.2.1.2.2.1.13.2 
       errors-in=.1.3.6.1.2.1.2.2.1.14.2 bytes-out=.1.3.6.1.2.1.31.1.1.1.10.2 packets-out=.1.3.6.1.2.1.31.1.1.1.11.2 discards-out=.1.3.6.1.2.1.2.2.1.19.2 errors-out=.1.3.6.1.2.1.2.2.1.20.2 

 2  RS name=.1.3.6.1.2.1.2.2.1.2.3 actual-mtu=.1.3.6.1.2.1.2.2.1.4.3 mac-address=.1.3.6.1.2.1.2.2.1.6.3 admin-status=.1.3.6.1.2.1.2.2.1.7.3 oper-status=.1.3.6.1.2.1.2.2.1.8.3 bytes-in=.1.3.6.1.2.1.31.1.1.1.6.3 packets-in=.1.3.6.1.2.1.31.1.1.1.7.3 discards-in=.1.3.6.1.2.1.2.2.1.13.3 
       errors-in=.1.3.6.1.2.1.2.2.1.14.3 bytes-out=.1.3.6.1.2.1.31.1.1.1.10.3 packets-out=.1.3.6.1.2.1.31.1.1.1.11.3 discards-out=.1.3.6.1.2.1.2.2.1.19.3 errors-out=.1.3.6.1.2.1.2.2.1.20.3 

แล้วดูเทียบกับเมนู /interface print

 #     NAME                                TYPE       ACTUAL-MTU L2MTU  MAX-L2MTU MAC-ADDRESS      
 0  R  ether1-internet                     ether            1500  1596       2026
 1  RS ether2-celty                        ether            1500  1596       2026
 2  RS ether3-switch                       ether            1500  1596       2026

ซึ่งของผม ether1-internet เป็นตัวที่ต่อกับ router ภายนอก ก็เลยจะต้องดูว่าเบอร์ 0 นั้น oid ที่แสดงข้อมูล internet คืออะไร ก็จะเห็นว่า

  • Bytes in: .1.3.6.1.2.1.31.1.1.1.6.1 (Download)
  • Bytes out: .1.3.6.1.2.1.31.1.1.1.10.1 (Upload)

ซึ่งเราจะจดไว้ใช้ในโค้ดเราต่อไป

Raspberry Pi

สำหรับบน Raspberry Pi นั้นของที่ผมมีอยู่จะเป็น Model B+ ค่อนข้างเก่าแต่ก็ไม่ได้จำเป็นต้องใช้อะไรแรงอยู่แล้ว (งานแค่นี้ Arduino ก็คงทำได้ถ้ามันมี library) โดยใช้ดิสโตรโปรดของผมคือ Arch Linux ARM

โดยโค้ดจะประมาณนี้


import time import socket import struct from easysnmp import Session SLEEP_DURATION = 2 session = Session(hostname='192.168.2.1', community='local', version=2) client = socket.socket(type=socket.SOCK_DGRAM) last_download = 0 last_upload = 0 while True: download = int(int(session.get('.1.3.6.1.2.1.31.1.1.1.6.1').value)/SLEEP_DURATION) upload = int(int(session.get('.1.3.6.1.2.1.31.1.1.1.10.1').value)/SLEEP_DURATION) if last_download != 0: print(download - last_download, upload - last_upload) client.sendto(struct.pack('<2I', download - last_download, upload - last_upload), ('192.168.3.7', 1)) last_download = download last_upload = upload time.sleep(SLEEP_DURATION)

คร่าวๆ ก็คือโปรแกรมจะอ่่านค่า OID ทั้ง 2 ตัวจาก 192.168.2.1 แล้วส่งไปให้ 192.168.3.7 UDP port 1

การนำไปติดตั้งบน Raspberry Pi ก็จะต้องติดตั้ง Python และ easysnmp เพิ่มเติม ดังนี้

# pacman -S python gcc net-snmp python-pip
# pip install easysnmp

M5Stack

ถัดมาเราจะเขียนโปรแกรมสำหรับรับข้อมูลมาแสดงกัน ก็จะประมาณนี้

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiUdp.h>

const char *WIFI_SSID = "WHS-IoT";
const char *WIFI_PASSWORD = "";
const int SERVICE_PORT = 1;
const int MAX_DOWNLOAD = 35 * 1000000 / 8;
const int MAX_UPLOAD = 6 * 1000000 / 8;
const int GRAPH_WIDTH = 1;

typedef struct netPacket {
  unsigned int download;
  unsigned int upload;
} netPacket;

WiFiUDP udp;
netPacket data;
int graphPos = 0;
int graphOriginY, downloadHeight, uploadHeight;

#define LCD_WIDTH M5.Lcd.width()
#define LCD_HEIGHT M5.Lcd.height()
#define FONT_BASE 31

void wifiConnect(){
  int wifiStatus = WL_IDLE_STATUS;
  bool firstTry = true;

  while(wifiStatus != WL_CONNECTED){
    if(!firstTry){
      switch(wifiStatus){
        case WL_NO_SHIELD:
          M5.Lcd.println("E: No module");
          break;
         case WL_CONNECT_FAILED:
          M5.Lcd.println("E: Connection fail");
          break;
         case WL_CONNECTION_LOST:
          M5.Lcd.println("E: Connection lost");
          break;
         case WL_DISCONNECTED:
          M5.Lcd.print(".");
          break;
         default:
          M5.Lcd.printf("E: %d\n", wifiStatus);
      }
    }else{
      M5.Lcd.printf("Connecting to %s\n", WIFI_SSID);      
    }

    wifiStatus = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    delay(5000);
    firstTry = false;
  }
  M5.Lcd.println();
  M5.Lcd.println(WiFi.localIP());

  graphPos = 0;
}

void checkWifiConnected(){
  if(WiFi.status() != WL_CONNECTED){
    M5.Lcd.clearDisplay();
    M5.Lcd.setCursor(0, FONT_BASE);
    M5.Lcd.println("Wifi disconnected!");
    wifiConnect();
  }
}

void setup() {
  M5.begin();
  M5.Speaker.mute();
  M5.Lcd.setFont(&FreeSans12pt7b);
  M5.Lcd.setCursor(0, FONT_BASE);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.setRotation(1);
  M5.Lcd.setTextWrap(false);

  int titleSize = FONT_BASE * 2;
  graphOriginY = titleSize + ((LCD_HEIGHT - titleSize) * 3 / 4);
  downloadHeight = graphOriginY;
  uploadHeight = LCD_HEIGHT - graphOriginY;

  Serial.printf("Graph origin %d\nDownload height %d\nUpload height %d\n", graphOriginY, downloadHeight, uploadHeight);

  wifiConnect();
  udp.begin(SERVICE_PORT);
}

void loop() {
  checkWifiConnected();

  bool drawText = false;
  if(udp.parsePacket()){
    udp.read((char *) &data, sizeof(netPacket));

    Serial.printf("DL %d UL %d\n", data.download, data.upload);
    drawText = true;
  }
  M5.Lcd.fillRect(graphPos, FONT_BASE * 2, GRAPH_WIDTH, LCD_HEIGHT, BLACK);

  int downloadPercent = min(max(1, data.download * downloadHeight / MAX_DOWNLOAD), graphOriginY - (FONT_BASE * 2));
  M5.Lcd.fillRect(graphPos, graphOriginY - downloadPercent, GRAPH_WIDTH, downloadPercent, RED);

  int uploadPercent = max(1, data.upload * uploadHeight / MAX_UPLOAD);
  M5.Lcd.fillRect(graphPos, graphOriginY, GRAPH_WIDTH, uploadPercent, YELLOW);

  if(drawText) {
    M5.Lcd.fillRect(0, 0, LCD_WIDTH, FONT_BASE * 2, BLACK);
     M5.Lcd.setCursor(0, FONT_BASE);
     M5.Lcd.printf("DL %.2f kb/s\nUL %.2f kb/s", (float) data.download/1000.0, (float) data.upload/1000.0);
  }

  graphPos = graphPos + GRAPH_WIDTH;
  if(graphPos > LCD_WIDTH){
    graphPos = 0;
  }

  delay(100);
}

Testing

ทีนี้เราก็จะต้องเทสว่ากราฟของเราขยับได้จริง โดยทำให้เน็ตมันโหลดขึ้นมา วิธีง่ายสุดคงเป็น speedtest

ถ้ากราฟขึ้นมาแบบนี้ก็เป็นอันใช้ได้

(Note: ความเร็วที่แสดงเป็น Kilobytes/s หรือจะตรงกับความเร็วที่แสดงในโปรแกรมดาวน์โหลด แต่โปรแกรม speedtest ส่วนมากจะแสดงผลเป็น Kilobits/s ฉะนั้นจะต้องคูณ 8 จากเลขที่แสดงบนจอด้วย)

สุดท้ายก็ไปแปะหน้า Switch ตามภาพ (M5Stack มีแม่เหล็กในตัว ส่วน power ดึงมาจาก Raspberry Pi เลย) คราวนี้ก็รู้สึกหรูขึ้นมา 10 เท่าเลย 555

Real world use

ติดเจ้าอุปกรณ์นี้มาสักพักแล้วครับ ประสบการณ์ที่เจอมาก็คือ

  • เวลากลับบ้านมาจะเห็น data use ขึ้นสูงเลยเพราะมือถือจะชอบ sync เมื่อชาร์จ + ต่อไวไฟ
  • config VPN failover อยู่ พอจะไปนอนเหลือบไปเห็นกราฟว่า upload วิ่งเต็มตลอด เลยรู้ตัวว่า BGP loop อยู่
  • ชอบดูตอนดู Streaming ถ้าเป็นพวก YouTube จะเห็นว่ามันโหลดหนักแป๊บเดียวแล้วเงียบเลย แต่ถ้าเป็น Twitch มันจะเบาๆ แต่ขึ้นตลอด
  • เมื่อวานเจอ Dota 2 โหลดแพทช์อีกแล้ว คราวนี้ก็รู้แล้วว่าเน็ตใช้อยู่จริงๆ

Future Improvements

ที่คิดว่าอยากทำคือ

  • ตอนนี้โปรแกรมอ่านค่ามีบั๊ก ถ้าค่าที่อ่านได้เกิน uint max (2^32) มันจะ serialize แล้วส่งไปไม่ได้ อาจจะต้องปรับ data size หรือทำท่าอื่นๆ
  • อาจจะมีค่าอื่นๆ ที่น่าเอามาแสดงผลอีก ก็ใช้ปุ่มบนตัว M5Stack เลื่อนเปลี่ยนโปรแกรมได้
  • การวาดกราฟทับดูยาก น่าจะมีวิธีที่ดีกว่านี้ (ตอนแรกในหัวนึกว่ามันจะเท่เหมือนเครื่องพล็อตกราฟชีพจร แต่อันนั้นหัวมันจะสว่างกว่าปกติ)

ปรากฏว่าพ่อเดินมาบอกว่า Raspberry Pi ก็ซื้อจอไว้นะ เอา Model B ไปใช้แทน B+ แล้วถอด M5Stack ไปทำอย่างอื่นเถอะ T_T ลดสเปคทุกอย่างไม่พอแถมต้อง rewrite อีกด้วย