ארכיון עבור הקטגוריה תכנות

הפוסט הזה מיועד למתכנתי ג'אווה שעובדים עם Eclipse.

אז אתם מתחילים פרוייקט, מפתחים, משתמשים בכל מני JARים מפרוייקטים אחרים בתוך הסביבת עבודה, מוסיפים פרוייקטים אחרים לרשימת התלויות של הפרוייקט שלכם, והכל עובד בתוך Eclipse.
ואז אתם צריכים לשחרר JAR שירוץ מחוץ לסביבת הפיתוח שלכם.
פה יש כמה אפשרויות:
1. להשתמש באופצית הExport JAR של Eclipse.
2. ליצור build.xml לפרוייקט

לפרוייקטים פשוטים, האפשרות הראשונה תספיק, אבל היא בהחלט לא עושה נעים בבטן. כדי ליצור את הJAR חייבים את Eclipse, וזה לא מאוד מיקצועי.
האפשרוית השניה היא מה שכמעט כולם עושים:
בדרך כלל מעתיקים build.xml מהפרוייקט השכן, ומתחילים לעקם אותו עד שיתאים לפרוייקט הנוכחי.
על הדרך עוד פעם מוסיפים - הפעם לbuild.xml החדש - את המסלול לJARים שהפרוייקט צריך כדי להתקמפל, ואולי גם את המסלול לספריות הbin של פרוייקטים אחרים שהפרוייקט הזה צריך, או אולי פשוט קריאה לקובץ הbuild.xml של הפרוייקטים האלו, ואז שימוש בתוצרים שלו.
אחרי זמן שיקח בין חצי שעה לחצי יום, תלוי בבלאגן שיש לכם בפרוייקט, כנראה תהיה לכם מערכת build עובדת לפרוייקט, שעושה בדיוק מה שאתם רוצים.
תהיה לכם תחושה נעימה בבטן, כי תוכלו לבנות את הפרוייקט שלכם מחוץ לEclipse (וככה גם אחרים אם בניתם את העסק נכון).

אבל מה, הפרוייקט ממשיך לחיות:
עם הזמן, נוספים לו תלויות בJARים נוספים, תלויות בפרוייקטים נוספים בסביבת העבודה וכו', ואז מערכת הבילד המדוגמת שלכם נשברת, ואתם מרגישים פיכס, כי שוב אי אפשר לבנות את העסק מחוץ לEclipse עד שתתקנו את הבילד.
הפעם בדרך כלל העידכון של הבילד הוא יותר פשוט, כמה דברים קטנים והכל עובד שוב.

עכשיו, תכפילו את כל העסק הזה במספר הפרוייקטים שאתם מפתחים אקטיבית על פני כמה שנים, ומתחיל להיות פה משהו די מעצבן.
הרבה קבצי build.xml שעושים כמעט אבל לא בדיוק את אותו דבר, והרבה התעסקות איתם ברגע שמשהו משתנה.

המפתח העצלן כבר מזמן שאל את עצמו: "מה, אין דרך אחרת?"
הרי לרוב הפרוייקטים, Eclipse מכיל את כל המידע שצריך בשביל לבנות אותם.
מעבר לזה, המידע הזה תמיד נכון בהגדרה כי אחרת לא תוכלו לפתח כלום וזה יהיה הדבר הראשון שתתקנו ברגע שתעשו איזה שינוי.
Eclipse שומר את רוב המידע בתוך קבצי ה.claspath בתוך כל פרוייקט (וגם קצת בתוך קבצי ה.project). הקבצים האלו הם קבצי XML פשוטים למדי, שלא השתנו משמעותית מאז ימי Eclipse הראשונים.
אז הנה רעיון:
מה אם במקום לשכפל את המידע גם בEclipse וגם בbuild.xml, ניצור build.xml אחד שקורא את המידע על הפרוייקטים מEclipse, מבין מה סדר הבניה הנכון של הפרוייקטים, איזה JARים צריך לכל פרוייקט ובונה את העסק לפי זה?
אותו build.xml קסום ומופלא יעבוד כמעט לכל פרוייקט Java שפותח בתוך Eclipse בצורה שקופה, בלי שום התעסקות ותחזוקה של קובץ build.xml ספציפי לפרוייקט.
נשמע טוב מכדי להיות אמיתי?
ובכן, שחררתי מערכת כזו בדיוק, ולמערכת קוראים ebuild.
התחלתי לפתח אותה לפני שנים, והיא ליוותה אותי דרך ארבע מקומות עבודה עד עכשיו (למעשה דרך כל מקומות העבודה שהיו לי בתחום ההיטק).
המערכת משוחררת תחת רשיון FreeBSD (רשיון תעשו מה שבזין שלכם, לא מזיז לי) ודף הבית שלה הוא http://ebuild.firefang.net.
בגדול, כדי להשתמש בה, מה שצריך לעשות זה:

  • לקחת את הקוד שלה ולהכניס לפרוייקט נפרד בסביבת העבודה.
  • להעתיק את example-common.properties לcommon.properties (הוא מכיל הגדרות ספציפיות למחשב שלכם, למרות שכרגע אין מה להתעסק איתו ברוב המקרים).
  • ליצור קובץ build.properties בתוך הפרוייקט שאתם בונים (מאוד פשוט ומינימלי)
  • להריץ עם ant -Dproject=YOUR_PROJECT, כאשר YOUR_PROJECT הוא השם שם הפרוייקט שלכם בסביבת העבודה.
  • לקחת את התוצרים האיכותיים מספרית הbuild שנוצרה תחת הפרויקט.

התוצר יהיה JAR שניתן להריץ בעזרת java -jar file.jar, וכן זיפ שכולל את הJAR ואת הספריות הדרושות כדי להפיץ את התוכנית.

נכון לכרגע, המערכת די בסיסית ומסוגלת לבנות פרוייקטי Eclipse בתוך סביבת העבודה (Workspace) שלכם.
בהמשך אני מתכנן להוסיף אפשרות לתייג פרוייקטם ולבנות ישר מCVS/SVN בלי לגעת בקוד בסביבת העבודה (זה דרוש כדי לשחרר גרסאות בצורה מסודרת).
בנוסף, אני מתכנן להוסיף אפשרות לebuild-hook.xml אופציונלי שיהיה ספציפי לפרוייקט שיכיל כל מני דברים שבאמת ספציפיים לפרוייקט מסויים.

יש כמה דברים שכדאי לדעת:
באופן כללי, עדיף לעבוד עם JARים שנמצאים בתוך בworkspace כי אז החיים של מי שרוצה לבנות את העסק הם יותר קלים.

  • ebuild תומך בזה בצורה טובה, אבל לא ברור לי אם הוא יתמודד עם JARים חיצוניים.
  • ebuild לא תומך בכל מני הרחבות מוזרות של eclipse, שמסתמכות על מידע שלא מופיע בקבצי ה.classpath. למשל 'user libraries'. אם אתם רוצים שהוא יעבוד בשבילכם, תעבדו פשוט.

זהו.
אני אשמח אם הרבה אנשים ישתמשו בebuild, ידווחו על בעיות ואולי אפילו ישלחו תיקונים.

לפני כמה שבועות פרסמתי פוסט על אופטימיזציות.
הנה ההמשך.
דוגמא מעשית, שיפור ביצועים של שאילתות בFireStats (לגרסא 1.6)
מנגנון מקובל לשיפור ביצועים הוא מטמון (cache). הרעיון פשוט : בפעם הראשונה ששואלים אותנו משהו אנחנו מחשבים אותו וזוכרים את התוצאה. בפעם השניה אנחנו משתמשים בתוצאה שחישבנו קודם.
כשבודקים ביצועים, חשוב לוודא שהבדיקה נותנת תוצאות זהות בשתי הרצות שונות. במקרים רבים יש מנגנוני מטמון יגרמו לבדיקה לרוץ הרבה יותר מהר בפעם השניה, ובדרך כלל הם רק מפריעים בסיטואציות של בדיקת ביצועים כי אנחנו רוצים למדוד את השיפור שנובע מהקוד שלנו, ולא מזה שהCache הכיל את התוצאה שכבר חישבנו בהרצה הראשונה.
דוגמא לכך היא הCache של MySQL: ההרצה השניה של שאילת תמיד תהיה הרבה יותר מהירה מההרצה הראשונה, כי MySQL זוכר את התוצאות מהפעם הראשונה ואם שום דבר בנתונים לא השתנה הוא פשוט מחזיר את אותה תוצאה.

כדי לבטל את אותו Cache, אפשר להשתמש בפקודת הMySQL:

SET GLOBAL query_cache_size = 0;

FireStats תומך בגרסאות MySQL ממשפחת 4.0, 4.1 ו5.0, כאשר ההבדל בין 4.0 ל4.1 כל כך משמעותי שיש שאילתות בFireStats שכתובות אחרת לכל אחת מהגרסאות.
נסיון לשיפור ביצועים, במיוחד כזה שמבוסס על שינויים בסכמת הנתונים יצריך שינויים בקוד שמטפל ב4.0 ובקוד של 4.1, לכן כדאי לבדוק שהכל עובד על השרת הרלוונטי. בנוסף, מכיוון שרוב (60%) משתמשי FireStats עובדים עם MySQL 5.X, כדי גם לבדוק את השיפור על השרת הזה.
כדי לבצע את זה, הרמתי שלושה שרתי MySQL, נציג אחד מכל משפחת גרסאות, והכנסתי לכל שרת חבילת נתונים די כבדה (וזהה בין השרתים) שמבוססים על סטטיסטיקות מהשרת שלי.
ובכל אחד מהשרתים ביטלתי את מטמון השאילתות (query_cache_size).
יצרתי טבלא בגנומטיק (כמו אקסל), שנראית ככה (תת טבלא כזו לכל בדיקה שאני רוצה)
fs-opt.jpg

נניח שהפונקציה שאני רוצה לשפר היא:

function foo()
{
        $sql = "SELECT * from …";
        return query($sql);
}

קודם כל הכפלתי את הקוד והוספתי הדפסה של שאילת הSQL שנשלחת לשרת:

function foo()
{
        if (true) // old code
        {
                $sql = "SELECT * from …";
                echo $sql; return;
                return query($sql);
        }else{
                $sql = "SELECT * from …";
                echo $sql; return;
                return query($sql);
        }
}

מכאן זו היתה שיטת העבודה:
1. הרצה של הקוד במצב הישן.
2. העתקה של השאילתה שהודפסה, והרצה שלה ישירות על כל אחד משלושת בסיסי הנהנתונים. ושמירה של הזמן שנדרש בטבלא מסודרת.
3. מעבר למצב חדש (פשוט לשנות את הfalse לtrue בראש הפונקציה), אופטימיזציה של השאילתה וחזרה על הבדיקה מול כל אחד מהשרתים, ושוב שמירה של התוצאות בטבלא, הפעם בעמודה של תוצאות אחרי אופטימיזציה.

העבודה השיטתית עזרה לי לא לשכוח דברים תוך כדי, ולוודא שאכן שיפרתי את התוצאות לפני שאני רץ להכניס את הקוד למערכת.
בנוסף, מכיוון שגם הקוד החדש וגם הישן היו לי מול העיניים, יכלתי לוודא שהם עושים בדיוק את אותו דבר.
חשוב לשים לב שלא ערבבתי אופטימיזציה של קוד הPHP עם שאילות הMYSQL. זה מבלבל מספיק גם ככה.

אז מה בעצם עשיתי?
פיירסטטס תומך בכמה אתרים בו זמנית, ולכן יש בו מזהה אתר. בהתחלה שמרתי את מזהה האתר בטבלאת הכניסות, בשלב מסויים הבנתי שאני צריך מזהה אתר גם בטבלאת הכתובות (URLים), כי הרי כל URL שיך לכל היותר לאתר אחד.
אז הוספתי את העמודה, אבל שכחתי למחוק את העמודה מטבלאת הכניסות.
מצב כזה של כפילות נתונים הוא לא מומלץ לפי הגישה המקובלת בתכנון בסיסי נתונים, כי הוא מאפשר מצבים של חוסר עקביות במידע.
באחת הגרסאות האחרונות של פיירסטטס תיקנתי את המעוות: הסרתי את העמודה מטבלאת הכניסות ותיקנתי את כל הקוד שהושפע מזה לבצע join עם טבלאת הכתובות כשהוא צריך גישה לאתר.
על פניו הדבר הנכון לעשות: אבל זה יצר בעיה חדשה:
כמעט כל השאילתות דרשו עכשיו join נוסף. עבור משתמשים עם בסיסי נתונים קטנים (נניח עד חצי מליון כניסות בטבלאת הכניסות ועד 50 אלף כתובות בטבלאת העסק עבד סביר, אבל עבור משתמשים עם בסיסי נתונים גדולים יותר העומס על השרת התחיל לגדול בצורה חדה ככל שנפח הנתונים עלה.
הפתרון שלי הוא לחזור בי מהדבר "הנכון" שעשתי קודם, כלומר להחזיר את עמודת הsite_id לטבלאת הכניסות, שתשמש לצרכי פילטור בזמן השאילות כדי להמנע מהjoin עם טבלאת הכתובות.

שיפור נוסף נבע משינוי מבני של השאילות ככה שיעבדו בצורה יותר יעילה.
כדי לעשות את זה, חשוב להבין מה עושה בסיס הנתונים כדי לחשב את השאילתה.
לשם כך אפשר להשתמש בפקודה explain, שמחזירה תיאור של האסטרטגיה שבה בסיס הנתונים יחשב את התוצאות.
explain מחזיר תוצאות די קריפטיות, לא משהו שתבינו בלי לקרוא את התיעוד.
בכך מקרה, חשוב לשים לב לעובדות הבאות:
בכל שלב, MySQL משתמש באינדקס אחד בלבד לכל היותר (אם יש אינדקס מתאים). ככל שהאינדקס יהיה יותר ספציפי ככה הביצועים יכולים להיות טובים יותר.
(יתכן שגרסאות עתידיות של MySQL ידעו להשתמש בכמה אינדקסים בשלב, אבל לדעתי זה עדיין לא פה).

ככה נראית הטבלא שלי אחרי שמילאתי אותה:

Query name MySQL Version 4.0.17 4.1 5.0.51
Num page views for all hits in site=1 Original query time sec 1.20 0.93 0.84
Optimized query time sec 0.10 0.06 0.06
Improvement % 91.67% 93.55% 92.86%
Num page views for all hits in all sites Original query time sec 1.17 0.91 0.84
Optimized query time sec 0.11 0.08 0.08
Improvement % 90.60% 91.21% 90.48%
Num page views for for last last 14 days for all sites Original query time sec 0.19 0.16 0.16
Optimized query time sec 0.12 0.09 0.11
Improvement % 36.84% 43.75% 31.25%
Num all unique visitors for site_id = 1 Original query time sec 2.93 1.24 1.13
Optimized query time sec 1.75 0.68 0.72
Improvement % 40.27% 45.16% 36.28%
Num all unique visitors for all sites Original query time sec 2.09 1.24 1.15
Optimized query time sec 1.26 0.47 0.47
Improvement % 39.71% 62.10% 59.13%
Num unique visitors for last 14 days for all sites Original query time sec 0.32 0.18 0.18
Optimized query time sec 0.28 0.16 0.15
Improvement % 12.50% 11.11% 16.67%
Recent referrers for all sites Original query time sec 2.54 3.01 3.01
Optimized query time sec 2.30 1.54 1.78
Improvement % 9.45% 48.84% 40.86%
Recent referrers for site_id = 1 Original query time sec 2.48 2.71 2.76
Optimized query time sec 2.65 1.65 1.90
Improvement % −6.85% 39.11% 31.16%
Search terms for site_id = 1 Original query time sec 1.67 2.14 2.75
Optimized query time sec 1.67 0.84 0.91
Improvement % Not optimized 60.75% 66.91%
Popular pages (all) Original query time sec 1.88 4.67 4.80
Optimized query time sec - 1.73 1.96
Improvement % Not optimized 62.96% 59.17%
Popular pages (site_id = 1) Original query time sec 1.93 4.07 4.09
Optimized query time sec - 1.88 2.11
Improvement % Not optimized 53.81% 48.41%
get questagents (180 days, site_id = 1) Original query time sec 6.89 2.90 2.35
Optimized query time sec 5.07 1.00 1.10
Improvement % 26.42% 65.52% 53.19%
Countries (20 countries, 180 days, site_id = 1) Original query time sec 1.95 1.08 1.08
Optimized query time sec 0.30 0.20 0.23
Improvement % 84.62% 81.48% 78.70%
Hits table (100) Original query time sec 0.30 0.61 0.42
Optimized query time sec - - -
Improvement % Not optimized Not optimized Not optimized

מי שמצליח להצדיק את התוצאה הזו מMySQL יזכה בחופשה בקאריבים:

CREATE TABLE t(a varchar(10));
INSERT INTO t(a) VALUES('aaa'),('bbb'),('ccc'),('ddd');
SELECT * FROM t WHERE a IN (666,'a');
+——+
| a    |
+——+
| aaa  |
| bbb  |
| ccc  |
| ddd  |
+——+
 

כולם יודעים שC יותר מהירה מג'אווה, נכון?
פרוייקטים רציניים של גריסת מספרים (Number crunching) כמו עיבוד תמונה בזמן אמת, זיהוי קול, רינדור, דחיסה, קידוד ווידאו וכו בדרך כלל נכתבים בC (או C++).
בהינתן שתי פיסות קוד שעושות בדיוק את אותו דבר, מעניין לראות את במה מתבטא היתרון של C על ג'אווה.
למה אפשר לצפות ליתרון?
כי ג'אווה רצה מעל JVM, והJVM מוסיף תקורה, ברור שC תרוץ יותר מהר כי היא רצה ישר על הCPU ולא דרך הJVM.

הנה שתי פיסות קוד, אחת בג'אווה ואחת בC. שתי התוכניות מאתחלות שתי מטריצות גדולות ומכפילות אותן אחת בשניה, הקוד בהחלט לא יעיל במיוחד ברמת האלגוריתם, אבל הוא זהה מבחינה מימושית.

הנה הקוד:
תוכנית C:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
   int i,j,k;
   int N = 2500;
   printf("N = %d\n", N);
   double *A = malloc(N*N*sizeof(double));
   double *B = malloc(N*N*sizeof(double));
   double *C = malloc(N*N*sizeof(double));
   double *bj = malloc(N*sizeof(double));
     for (i = 0; i < N; i++)
       for (j = 0; j < N; j++)
       {
           int n = i*N+j;
           A[n] = i * j;
       }

   for (i = 0; i < N; i++)
       for (j = 0; j < N; j++)
           B[i*N+j] = i * j * j;

   // order 7: jik optimized ala JAMA
   for (j = 0; j < N; j++) {
       for (k = 0; k < N; k++)
           bj[k] = B[k*N+j];
       for (i = 0; i < N; i++) {
           double s = 0;
           for (k = 0; k < N; k++) {
               s += A[i*N+k] * bj[k];
           }
           C[i*N+j] = s;
       }
   }
   printf("done\n");
   return 0;
}

תוכנית ג'אווה:

public class Matrix
{
    public static void main(String[] args)
    {
        int i,j,k;
        int N = 2500;
        System.err.println("N = " + N);
        double A[] = new double[N*N];
        double B[] = new double[N*N];
        double C[] = new double[N*N];
        double bj[] = new double[N];
        for (i = 0; i < N; i++)
            for (j = 0; j < N; j++)
            {
                int n = i*N+j;
                A[n] = i * j;
            }

        for (i = 0; i < N; i++)
            for (j = 0; j < N; j++)
                B[i*N+j] = i * j * j;

        // order 7: jik optimized ala JAMA
        for (j = 0; j < N; j++) {
            for (k = 0; k < N; k++)
                bj[k] = B[k*N+j];
            for (i = 0; i < N; i++) {
                double s = 0;
                for (k = 0; k < N; k++) {
                    s += A[i*N+k] * bj[k];
                }
                C[i*N+j] = s;
            }
        }
        System.err.println("done");
    }
}

מי לוקח התערבות של בכמה C עוקפת את ג'אווה בזמן הריצה של זה?
נקמפל ונבדוק:

$javac Matrix.java
$gcc Matrix.c -o matrix
$ date;java Matrix;date;./matrix;date
Thu Jun 26 08:42:10 IDT 2008
N = 2500
done
Thu Jun 26 08:42:54 IDT 2008
N = 2500
done
Thu Jun 26 08:44:31 IDT 2008

לתוכנית בג'אווה לקח לקח 44 שניות ולתוכנית בC לקח 107 שניות.
מש"ל.

אה, רגע. רצינו להראות שC יותר מהירה!
טוב, מסתבר שלא כדאי לקחת דברים כמובנים מאליהם, גם אם כולם יודעים שהם נכונים.
אם אתם חושבים שרימיתי, תריצו בעצמכם. בדקתי על שני מחשבים, אחד עם שתי ליבות של 3GHZ, ואחד עם ארבע ליבות של 2.4GHZ (כמובן שהראשון הוביל בכמה אחוזים טובים, אבל היחס נשמר).
השתמשתי בJava 1.6.06.

לדעתי התופעה הזו נובעת מההתקדמות המדהימה של סביבת הריצה של ג'אווה בתחום הHotspot.
Hotspot היא טכנולוגיה שמקמפלת חלקים "חמים" בתוכנית בזמן, אבל בזמן ריצה. מכיוון שזמינות לHotspot סטטיסטיקות בזמן הריצה הממשי של התוכנית היא יכולה לשנות את הקוד ככה שירוץ בצורה אופטימלית לאור התנהגות של התוכנית ולא כנסיון מלומד לנחש מה יהיה יותר מהר מהתבוננות ושינוי הקוד, מה שעושה קומפיילר סטאטי.

עדכון:
קימפלתי את התוכנית C עם אופטימיזציה מקסימלית והתוצאה שלה השתפרה פלאים:

gcc -O3 Matrix.c
$ date;./matrix;date
Thu Jun 26 11:26:00 IDT 2008
N = 2500
done
Thu Jun 26 11:26:29 IDT 2008

הפעם התוצאה של C היא 29 שניות.
טוב משמעותית מקודם, וגם יותר מהיר בכ30% מג'אווה.

עדכון 2:
שמתי לב שקוד שקומפל עם javac איטי מקוד שקומפל בeclipse. נחשתי שeclipse מקמפל עם jikes (אני לא בטוח בזה).
ניסיתי עם jikes והתוצאה השתוותה, תיקו 29 שניות.

$ javac Matrix.java ; time java Matrix
N = 2500
done

real 0m42.854s
user 0m55.335s
sys 0m26.214s

$ jikes –bootclasspath /usr/lib/jvm/java-6-sun-1.6.0.06/jre/lib/rt.jar Matrix.java ; time java Matrix
N = 2500
done

real 0m29.463s
user 0m29.366s
sys 0m0.108s

אגב, זו תוצאה מדהימה שכדאי שכל מפתח ג'אווה יכיר.

מי רוצה לשפר את התוצאות עוד?

בצירוף מקרים קוסמי שחררתי היום גרסאות חדשות לשלושה פרוייקטים בלתי תלויים:
FireStats 1.5.9-RC3:
FireStats 1.5 מתייצב, ורוב הסיכויים שהגרסא הבאה תהיה הגרסא היציבה של הענף הזה.
רשימת השינויים נמצאת כאן.

WPMU Plugin Commander 1.1.0

WPMU Plugin Commander הוא תוסף ניהוי תוספים לWPMU, שמוסיף כמה תכונות מאוד נדרשות לWPMU (אפשרות לבחור איזה תוספים משמשים יכולים להפעיל ולכבות, אפשרות להפעיל אוטומטית תוספים עבור בלוגים חדשים ועוד)
אתמול בלילה קיבלתי תרומת קוד שמוסיפה תמיכה בריבוי שפות לPlugin Commander, וכן שינויי עיצוב שגורמים לא להיות הרבה פחות מכוער.
בנוסף, התורמת של הקוד תרמה גם תרגום לרוסית של התוסף.

Antenna 1.1.0-beta
קיבלתי תרומת קוד גדולה מצוות MTJ, הקוד מממש את הPreprocessor שכתבתי בANTLR 3.0, מה שיאפשר הכנסה של הPreprocesror לתוך MTJ.
MTJ הוא פרוייקט שמטרתו להכניס תמיכה בפיתוח קוד J2ME בEclipse. הפרוייקט יתבסס על EclipseME, שהוא הסטנדרט דה-פקטו לפיתוח J2ME עם Eclipse. הPreprocessor שלי כבר נמצא בתוך EclipseME די הרבה זמן, אבל היתה בעיה עם הרשיון של ANTR 2.7.7 שבו השתמשתי כדי לפתח אותו, ו Eclipse legal לא אישרו את הכנסת הקוד שלי לMTJ מהסיבה הזו.
הצוות של MTJ בחר לבצע Porting של הPreprocessor כך שיעבוד עם ANTLR 3.0 כי הרשיון שלו תואם את זה של Eclipse.
לפני כמה ימים הצוות קיבל אישור מEclipse legal לתת לי את הקוד שהם יצרו כדי שאני אקלוט אותו לתוך Antenna.
הקוד היה איכותי, ועבר את כל בדיקות היחידה שהגדרתי כבר, מה שנתן לי ביטחון גבוה שהוא עובד כמו שצריך. אחרי כמה שעות עבודה, הקוד הוטמע והיום שחררתי גרסא חדשה של Antenna שמשתמשת בPreprocessor החדש כברירת מחדל.

איכות תוכנה נמדדת בכמה פרמטים:
נכונות (correctness): האם התוכנה עושה מה כל מה שהיא צריכה לעשות, ובצורה נכונה?
קלות תחזוקה (maintainability) : כמה קל למצוא ולטפל בבאגים? כמה קל להרחיב את המערכת?
ביצועים (performance) : האם התוכנה עובדת מהר מספיק?
המדדים האלו נוטים לבוא אחד על חשבון השני.
קוד נכון וקל לתחזוקה הוא בדרך כלל פחות יעיל.
קוד נכון ויעיל הוא בדרך כלל קשה לתחזוקה.
קוד יעיל וקל לתחזוקה.. אין דבר כזה :).

שיפור ביצועים בתוכנה הוא אחד הנושאים שכל מפתח מגיע אליו בשלב מסויים. בדרך כלל מוקדם בהרבה ממה שצריך.
הבעיה עם שיפור ביצועים היא שהוא מעלה את מורכבות הקוד, מה שפוגע אוטומטית בקלות התחזוקה, ומעלה את הקושי בשמירה על נכונות.
חכמים ממני כבר אמרו שאופטימיזציה מוקדמת מדי היא שורש כל רע :
כשמשפרים ביצועים מוקדם מדי, קשה להוסיף אחר כך תכונות נוספות לאפליקציה, כי המורכבות של הקוד עולה.
הבעיה היא שגם שכבר מגיע הזמן לשפר ביצועים, רוב המפתחים מבזבזים את הזמן שלהם בשיפורים חסרי ערך שרק מעלים את מורכבות האפליקציה, תוך רווח שולי מאוד בביצועים, לפעמים רווח כל כך שולי שהוא בלתי מורגש.
כשבאים לשפר ביצועים, חשוב להשקיע את הזמן בשיפורים הנכונים.
הדרך הנכונה היא לא להסתכל על הקוד, למצוא פיסת קוד לא יעילה, לשפר אותה ולהכריז שהאפליקציה עובדת יותר מהר.
הדרך הנכונה היא קודם כל לאתר מועמדים לאוטימיזציה, לא לפי איפה שאפשר לעשות אופטימיזציה אלא לפי איפה שצריך והדרך לשם היא למדוד את ביצועי האפליקציה בטיפול במשימות בעייתיות מבחינת ביצועים (בין אם זה יהיה זמן איתחול, פעולה מסויימת בממשק המשתמש שמרגישה איטית או כל דבר אחר).
כשמודדים, חשוב להבין היטב באיזה חלק של הקוד התוכנית מבלה את רוב הזמן, זאת על ידי מדידות של תתי משימות בתוך אותה משימה בעייתית.
ברגע שהבנו את התנהגות האפליקציה ואיתרנו את הגורם או הגורמים לאיטיות, הגיע הזמן לנסות לשפר את הביצועיים.

בשלב הזה רוב המפתחים פשוט ירוצו וישכתבו חלקים מהקוד כדי לגרום להם לרוץ יותר מהר, בדרך כלל תוך העלאת המורכבות של הקוד. הבעיה היא שגם אם אכן יש שיפור ביצועים - איך נחליט אם הוא מצדיק את השינויים שעשינו?
המשמעות של השינויים האלו, היא כאמור עליה במורכבות הקוד, מה שיקשה עלינו להוסיף תכונות עתידיות לקוד ויקשה עלינו לאתר ולתקן באגים.
אולי השיפור בפועל הוא 2% בלבד ולא מצדיק את המחיר הזה?
כדי להחליט צריך להשוות את הביצועים לפני ואחרי השינוי, ולהחליט אם השיפור מצדיק את השינוי בקוד.
הדרך לעשות את זה היא ליצור בדיקה מוגדרת היטב שניתן להריץ שוב ושוב ולקבל מדידות זמן כמעט זהות בין הרצות שונות, ואז לראות בכמה אנחנו מצליחים לשפר את זמן הריצה של הקוד.
כדאי שהבדיקות יריצו בעיקר קוד אמיתי של התוכנית, למפתחים יש נטיה ליצור בדיקות סינטטיות שמראות שיפורי ביצועיים אסטרונומיים בנקודות מסויימת, אבל בתמונה הכללי הרבה פעמים השיפורים הם הרבה פחות משמעותיים.
דוגמא פשוטה:
קוד אמיתי מהתוכנית

// do stuff
doSomething();
// do more stuff
 

בדיקה סינטטית:

int start = time();
for (int i=0;i<1000000;i++)
{
     doSomething();
}
int elapsed = time() - start;
print "average time is " + (elapsed / 1000000);

צריך לשים שהבדיקה הסינטטית בודקת תזמון של פעולה אחת בזרימה של התוכנית, ויש עוד פעולות (do stuff וdo more stuff). ככה שגם אם נשפר את הביצועים של הבדיקה ב90% - אם הפעולות האחרות לוקחות בסך הכל שניה, וdoSomething לוקחת 10 מילישניות אז השיפור הוא זניח במקרה הזה.

בפוסט הבא בסדרה הזו אני אתאר דוגמא מעשית של אופטימיזציה של שאילתות MySQL

פיטר ברייט במאמר מרתק בארס-טכניקה על המעבר שלו מפיתוח לWin32 לפיתוח Cocoa (הAPI של Max OS X).
המאמר כולל סקירה היסטורית והשוואה של XP לMaxOS X; דוט-נט - הנסיון של מייקרוסופט להפוך את הAPI שלה למודרני. ועוד.
עדיין לא קראתי את כולו, אבל בינתיים הוא מרתק.
המאמר מחולק לשלוש חלקים:
חלק ראשון
חלק שני
חלק שלישי

הערה: יש סקר בסוף.

סקירה היסטורית
בראשית היה ASCII (האמת היא שהיו קידודים לפני ASCII אבל הם לא מעניינים אותנו).
אסקי נועד בעיקר לתווים באנגלית, הערך של 'A' הוא 65, הערך של רווח הוא 32 וכן הלאה. ASCII הוא קידוד 7 ביטים, מה שאומר שהוא משתמש ב128 אפשרויות מתוך 256 האפשרויות שנכנסות בbyte.
הבעיה עם אסקי היא שהוא לא כולל תווים של שפות אחרות - קירילית ועברית למשל.

Code page
הפתרון המתבקש הוא להשתמש בערכים 128-255 כדי לייצג את האותיות החסרות.
הבעיה היא שהרבה אנשים חשבו על הפתרון הזה בו זמנית, ומטבע הדברים היו הרבה טבלאות כאלו, לפעמים אפילו כמה בתוך אותה מדינה.
לא נחמד, כי מסמך שנכתב תוך שימוש בטבלא אחת לא הוצג כמו שצריך למי שהשתמש בטבלא אחרת.
בשלב מסויים הוגדרו סטנדרטים על ידי ארגון התקינה האמריקאי (ANSI) , שנקראו Code pages, מה שעזר למנוע הווצרות של טבלאות מיותרות חדשות.
הבעיה העיקרית עם הפתרון הזה הוא שאי אפשר לערבב שפות שמשתמשות בקודים שונים.
בנוסף, הוא נותן פתרון רק לשפות בעלות פחות מ128 אותיות.
באסיה הוגדר תקן בשם DBCS - Double byte character set, שנועד לתת פתרון לשפות האסיאתיות.
התקן הזה השתמש בקידוד באורך משתנה: חלק מהתווים היו באורך בייט אחד וחלק באורך שני בייטים, ובאופן כללי היה מבלבל למדי.

UNICODE
יוניקוד הוא שם הקוד לטבלא גדולה מאוד שמתאימה מספר לכל אות ידועה (וגם כמה משפות מומצאות כמו קלינגונית), מכיוון שיש טבלא אחת לכל השפות - אין בעיה לערבב בין שפות שונות.
מיתוס נפוץ הוא שניתן לייצג כל אות ביוניקוד בעזרת מספר בין 16 סיביות (או במילים אחרות, שיש פחות מ65536 אותיות ביוניקוד).
זה לא נכון, ולמעשה יש ביוניקוד גרסא 4.0 קרוב ל240,000 סימנים, מה שאומר שצריך לפחות 3 בתים כדי למספר את כל התווים ביוניקוד.
מחרוזת ביוניקוד היא בסך הכל סדרה של מספרים, כאשר כל מספר הוא המיקום של אות מסויימת בטבלא.
מקובל לסמן תו יוניקוד בסימן כמו U+0041, כאשר U+ אומר שזה יוניקוד והמספר שאחריו הוא קוד האות בבסיס הקסדצימלי.
לא במקרה, 128 התווים הראשונים ביוניקוד הם בדיוק אותם 128 התוים הראשונים באסקי וברוב קידודי הCode page.
המחרוזת hello ביוניקוד תכתב ככה:
U+0048 U+0065 U+006C U+006C U+006F
אם נשמור את זה, נקבל:

00 48 00 65 00 6C 00 6C 00 6F

או

48 00 65 00 6C 00 6C 00 6F 00

תלוי בשיטת בה אנחנו מקודדים ספרות בזכרון המחשב (Little endian או Big endian).
כדי להבחין בין שתי השיטות ישנה תוספת של התווים FE FF בתחילת מחרוזת יוניקוד (שתראה ככה או הפוך, לפי הEndianness של המכונה).
הסימון הזה נקרא Unicode Byte Order Mark, או בקיצור BOM - והוא גורם ללא מעט צרות לדפדפנים שכתובים רע.

UTF-8
באו האמריקאים ושאלו, מה אנחנו צריכים את האפסים האלו באמצע המחרוזת? הרי המחרוזת תופסת פי שתיים, והם גם מבלבלים תוכנות שמתייחסות ל0 בתור סימון לסוף המחרוזת (מקובל בC וC++).
וככה נולד UTF-8.
UTF-8 הוא שיטה לקידוד יוניקוד בקידוד בעל אורך משתנה.
UTF-8 encoding table
כשקוראים UTF-8, מסתכלים על התו הראשון ולפי הביטים הראשונים אפשר לדעת בדיוק על כמה בתים האות הבאה יושבת.
UTF-8 הפך לסטנדרט המקובל ביותר לקידוד מחרוזות יוניקוד.

PHP ויוניקוד
PHP התמיכה של PHP 4 ו5 ביוניקוד חלקית ביותר.
מחרוזות בPHP הן בעצם סדרה של בייטים ולא יותר ולמרות שתמיד אפשר להשתמש במחרוזת PHP כדי לשמור מחרוזות בקידודים שונים - רק במקרה שהPHP קומפל עם תמיכה בmb_string יהיו לנו פונקציות מיוחדות לטיפול במחרוזות מרובות בתים.
פתרון נוסף הוא להשתמש בספריה iconv, שמוסיפה לPHP יכולות המרה של קידודים, אבל היא לא מגיעה כברירת מחדל עם PHP ומי שרוצה תוכנה שתוכל לרוץ בקלות בכל מחשב ימנע ממנה.
בPHP 6 שמתבשל לאיטו צפויה תמיכה ביוניקוד, UTF-8 וכל זה, אבל זה עדיין לא שוחרר, ואם לשפוט לפי הקצב שלוקח לשוק לאמץ את PHP5 - אז PHP6 לא יהיה רלוונטי בשנים הקרובות למי שרוצה לשחרר תוכנה שתרוץ בכל מחשב.
מכיוון שהפונקציות הסטנדרטיות בPHP תומכות בעצם רק בקידוד שבו כל אות תופסת בייט אחד, הן יכולות ליצור בעיות מעניינות.
אם תקבלו מחרוזת שמקודדת בUTF-8, נניח "שלום", ותציגו אותה בדפדפן האורך שלה יהיה 4 אותיות. אם תשתמשו בפונקצית הPHP לחישוב אורך של מחרוזות, תקבלו שהאורך שלה הוא 8 תווים, כי כל אות מקודדת בשני בתים.
אם תנסו את אותו דבר על המחרוזת המעורבת "שלום SHALOM", תקבלו שהאורך הוא 8 + 1 + 6 = 15.
לעומת זאת, אם תשתמשו בmb_strlen תקבלו את האורך הנכון.
בעיה נוספת היא בעיה של חיתוך מחרוזות UTF-8.
אם נשתמש בפונקציה wordwrap לחיתוך מחרוזות UTF8, היא עלולה לחתוך אות בין שני הבתים שלה, ובעצם להעלים אותה. לא נעים.
הפתרון שלי היה לכתוב גרסא של wordwrap שעובדת על מחרוזות UTF-8.
אפשר להבין למה מפתחי PHP מתבלבלים כשהם מתעסקים עם מחרוזות בUTF-8.

המרות וקיבועים
למרות שרוב אתרי האינטרנט בימינו השכילו לעבור לUTF-8 לקידוד של מחרוזות, עדיין יש אתרים מסויימים שמשתמשים בקידודים מבוססי Code page. למשל - מנוע החיפוש של Walla מקודד לפעמים את מילות החיפוש בכתובת בקידוד עברית 1255 (עברית חלונות), ולפעמים בקידוד UTF-8. מאוד נחמד מצידם של המפתחים לפחות להעביר את הקידוד כחלק מהכתובת (e=hew לעברית 1255 וe=utf לutf8).
לא חסרות דוגמאות אחרות, בעיקר במנועי חיפוש מקומיים (yandex.ru, mail.ru שמשתמשים בקידוד קירילי 1251) ועוד.
מכיון שאני רוצה שמילות החיפוש יוצגו כמו שצריך בFireStats, צריך להמיר את הקידודים האלו לUTF-8.
הבעיה היא שכאמור - אין תמיכה מובטחת בiconv שמאפשר המרות כאלו, ולכן נאלצתי לכתוב בעצמי ספריית המרות קטנה מקידודי codepage כלשהם לקידוד UTF8. הספריה מסתמכת על טבלאות המרה שאפשר להשיג באתר הראשי של יוניקוד.
הרעיון של הספריה הוא להמיר באמצעות הטבלא את המחרוזת ליוניקוד, ואז לקודד אותה לUTF8.

וזו האגדה על אסקי יוניקוד וUTF-8.

מה דעתכם על המאמר?

View Results

Loading ... Loading …

רוצה עוד מאמרים טכניים?

View Results

Loading ... Loading …

קריאה נוספת

Joel on software במאמר מצויין על יוניקוד
מצגת על השרדות עם UTF-8
שאלות נפוצות על יוניקוד וUTF-8 בלינוקס ויוניקס

בהמשך לפוסט האחרון על בעיות רשת מוזרות, הנה עוד אחת.
אפליקציית ג'אווה שכתבנו עובדת טוב לרוב האנשים, אבל משום מה "נתקעת" בהתחברות לחלק מהאנשים.
בבדיקה, נראה שהאפליקציה מתחברת לשרת, מקבלת תשובה ממנו, אבל לא קוראת את התוכן של התשובה.
בבדיקה מעמיקה יותר, רואים שהאפליקציה ניגשת לשדה הContent-Length של התשובה, ומקבלת משם 1-, מה שמרמז על זה שאין בתגובת הHTTP שדה של Content-Length.
בדיקה קטנה עם WireShark (לשעבר Ethereal) מראה את זה:

HTTP/1.1 200 OK
Date: Thu, 31 Jan 2008 13:21:24 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 2.0.50727
Set-Cookie: ASP.NET_SessionId=j2hx2qvprzec3u55htsykh45; path=/; HttpOnly
Cache-Control: private
Content-Type: text/html
Content-Length: 20877


(המשך התגובה נחתך מחוסר עניין).

אז נראה שהכל בסדר, יש Content-Length בתגובה.
ובכל זאת, אפליקציית הJava מקבלת 1-, ובדיבוג מעמיק אין רמז לContent-Length באובייקט שמייצג את תגובת הHTTP.
אז מה קורה פה?

הצעד הבא הוא לבדוק TCP נקי, בלי טובות של Java בפירסור התגובה.
תוכנית קטנה שמתחברת בTCP לשרת הHTTP, שולחת את הבקשה ומדפיסה את התוצאה מהשרת הפיקה את הפלט הזה:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Thu, 31 Jan 2008 13:21:24 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 2.0.50727
Set-Cookie: ASP.NET_SessionId=j2hx2qvprzec3u55htsykh45; path=/; HttpOnly
Cache-Control: private
Content-Type: text/html
————–: —–

יש פה כמה דברים שצריך לשים אליהם לב:
1. פתאום יש לנו בתשובה Transfer-Encoding: chunked.
2. מה זה איפה שקודם היה לנו Content-Length? ————–: —– ?? איזה וודו שחור פועל פה?

יש לציין שבזמן שאני מקבל את הנתונים האלו בתוכנית, אני מקבל נתונים זהים למה שראיתי קודם WireShark, כלומר - המחשב מקבל את הנתונים בצורה תקינה, אבל התוכנית מקבלת אותם אחרי טיפול.

כדי לנטרל אפשרות שJava התחלקה על השכל, ניסיתי (בעזרתו של חבר מהעבודה) להריץ תוכנית שעושה את אותו דבר בדיוק, רק שכתובה בC#.
לא במפתיע, התוצאה זהה: אין Content-Length, במקום זה יש שורת מינוסים.
מסקנה, Java לא אשמה הפעם.

אז מה נשאר?
מי משחק בתעבורת הרשת במחשב? אולי זה ווירוס, ואולי זה אנטי וירוס; בכל מקרה מדובר בתוכנה נאלחת.
לא במפתיע, האשם הוא הSymantec Client Firewall, שסורק בזמן אמת את תעבודת הHTTP, מחרבש אותה קלות, ומעביר את הזבל לתוכניות התמימות.
ברגע שנטרלתי את הסריקה שלו את פורט 80, הכל הסתדר.

יש לי משהו לומר למהנדסים של סימנטק:
אתם אינוולידים, מי נתן לכם את הזכות לשבש את התעבורה?
תסרקו, זה בסדר, אבל אין שום הצדקה שבעולם שהתוכניות שקוראות מהרשת יחשפו לשינויים שתרמתם.
אם הייתם עובדים אצלי הייתי משבש לכם את המשכורת.

אם הייתי כותב ווירוסים, הייתי משתמש במידע הזה בכיף כדי לזהות שהמחשב מריץ את הFirewall של סימנטק, שמשאיר עקבות כמו חזיר בחנות בורקסים.

במסגרת מקצה אופטימיזציות לFireStats מימשתי מנגנון נוסף לקליטת כניסות.
המנגנון הרגיל בו כולם משתמשים היום בודק שהכניסה לא צריכה להיות מסוננת (כניסה של רובוט ידוע או IP מסונן למשל), ומכניס את הנתונים בצורה מנורמלת.
נירמול בסיס נתונים נועד למנוע כפילויות, מה שעוזר במניעת אנומליות ובחיסכון במקום. בFireStats הנרמול מתבטא בכך שכל כתובת נשמרת פעם אחת בטבלאת הURLים, כל UserAgent בטבלאת הUserAgents וכדומה. כאשר מכניסים את הנתון העיקרי של כל הכניסה, משתמשים במזהה של כל נתון מנורמל.
המשמעות של זה בזמן הכנסת הנתונים היא כזו:
הגיעה כניסה עם כתובת מסויימת, מפנה מסויים ודפדפן (UserAgent) מסויים. לכל אחד מהנתונים האלו מבצעים פחות או יותר את סדרת הפעולות הבאה:
הכנס לטבלא הרלוונטית עם INSERT IGNORE, מה שמונע שגיאה במקרה שהנתון כבר נמצא שם (הטבלאות מוגדרות לא לקבל רשומות כפולות).
בדוק מה המזהה של הנתון שהכנסנו (אם הוא נכנס, אז זה יהיה מזהה חדש, אם לא זה יהיה המזהה שנבחר בפעם הראשונה שהכנסנו את הנתון לטבלא).
לבסוף, הכנס שורה לטבלאת הכניסות תוך שימוש במזהים שמצאנו בצעדים הקודמים.
זה קצת יותר מורכב מזה כי גם צריך לזהות ולסנן כניסות מסויימות כאמור.

כל התהליך הוא איטי למדי.
יצרתי קובץ CSV עם 100,000 כניסות (מfirestats.cc) והשתמשתי בו למדידת הביצועים:
בשיטה של הכנסה מנורמלת של כל כניסה, הקצב מתחיל די לא רע עם 80 כניסות לשניה, אבל ככל שבסיס הנתונים מתמלא הוא יורד עד שמתייצב על 7-8 כניסות לשניה.
למרות שקצב כזה בהחלט מספיק לכל בלוג מצוי, הוא ממש לא מספק לאתרים רציניים יותר או לבלוגיות עתירות בלוגים.

כשהוספתי תמיכה בWPMU בFireStats 1.4, חזיתי (חודשים לפני שביצעתי את המדידות) שקצב הכניסות יכול להיות בעיה בבלוגיות והוספתי שיטה חדשה לקליטת נתונים.
במקום לקלוט את הנתונים בצורה מנורמלת, הנתונים נקלטים לטבלאת כניסות ממתינות בצורה לא מנורמלת עם INSERT DELAYED. המשמעות של הDELAYED היא שבסיס הנתונים מכניס את הנתונים בזמנו הפנוי, ולא מחזיק את הקורא עד שהנתון הוכנס ממש.
עדיין צריך לנרמל את הנתונים, ולכל כתבתי סקריפט PHP פשוט שעובר על הכניסות הלא מנורמלות בטבלא אחת אחת, ולכל אחת קורא לפונקציה הרגילה שקולטת נתונים ומנרמלת אותם, ולבסוף מוחק את הכניסה מטבלאת הכניסות הממתינות. (מנהל המערכת אחראי לדאוג שהסריפט ירוץ בפרקי זמן סבירים, למשל באמצעות cron).
אני שומע אתכם צועקים: כן, אבל זה יהיה איטי לפחות כמו קודם, אם לא יותר!
זה נכון, אבל לפחות זה מאפשר לתזמן את העיבוד של הכניסות לזמנים פחות עמוסים כמו הלילה.
חיסרון נוסף הוא שהמשתמשים כבר לא מקבלים את הנתונים בזמן אמת, אלא נתונים שנכונים נכון לזמן העיבוד האחרון של הכניסות בטבלאת הממתינים.

אחרי שמדדתי את הזמן שדרוש כדי להכניס 100,000 כניסות בשיטה הרגילה, כמובן שמדדתי את הזמן שדרוש בשיטה המעוכבת, שנועדה לשפר ביצועים.
מסתבר שבשיטה המעוכבת, FireStats קולט בסביבות ה1000 כניסות לשניה, אבל המילכוד הוא שהנתונים עדיין דורשים עיבוד כדי שיהיו שימושיים, והעיבוד יקר בדיוק כמו הטיפול הרגיל.

הצעד הבא הוא כמובן לשפר את הביצועים של העיבוד הנ"ל.
כבר מהרגע הראשון שכתבתי אותו, היה לי ברור שדרושה פה אופטימיזציה רצינית, והיה לי גם ברור שהיא תהיה מסובכת.
האופטימיזציה מתבססת על התובנה הבאה:
בתוך קבוצת כניסות שנקלטו בפרק זמן מסויים, יהיו חזרות רבות מאוד של כתובות, מפנים ודפדפנים.
מה אם במקום לטפל בהם אחד אחד, נטפל בהם בקבוצות? נניח 1000 כניסות בכל קבוצה?
במקום 1000 כתובות ו1000 מפנים, יהיו לנו משהו כמו 300 כתובות (שכוללים מפנים).
במקום משהו כמו 1000 דפדפנים יהיו לנו משהו כמו 50 או 100 דפדפנים.
עכשיו נצטרך להכניס את כל מה שחדש לבסיס הנתונים, לחלץ את המזהים שלהם, ולבסוף להכניס את הכניסות עצמן תוך שאנחנו משתמשים במזהים מקודם, רק שהפעם נכניס 1000 כניסות במכה במקום כניסה אחת.
המשמעות של זה היא בעצם לממש את הפונקציה שמכניסה כניסה בודדת - אבל לרוחב, כך שתהיה ברוחב של k כניסות (קבוע כלשהו).
המימוש של זה מורכב, ולמעשה לקח יותר מפי עשר שורות קוד מהמימוש התמים הקודם, אבל הביצועים סוכר:
מעיבוד של 8 כניסות בשניה, עליתי לעיבוד של 600 כניסות לשניה.
שיפור של פי 75!

עכשיו נשאר רק לבדוק נכונות.
אם אני לוקח חבילת כניסות נתונה, ומכניס אותה השיטה המיידית אך האיטית, או מכניס אותה בשיטה המעוכבת אך המהירה התוצאה בבסיס הנתונים צריכה להיות זהה.
לא דומה, זהה.
בהתחלה השתמשתי בmysqldump (תוכנית שמגיעה עם mysql כדי לגבות את בסיס הנתונים) כדי לשמור שני קבצים, מתוך כוונה להשוות אותם.
מסתבר שזה לא היה רעיון כל כך מוצלח, כי היא הפיקה שורות ארוכות מאוד מאוד, שגרמו לרוב תוכנות ההשוואה שניסיתי להתבלבל או פשוט להיות לא שימושיות.
הצלחתי לדעת שיש בעיות, אבל לא הצלחתי לזהות אותן.

אחרי חיפוש קצר מצאתי את phpsqldiff, תוכנת קוד פתוח שמאפשרת השוואה של טבלאות.
phpmysqldiff מסוגלת לומר בדיוק מה נוסף, מה ירד ומה השתנה בין שתי טבלאות, ובעזרתה מצאתי בדיוק מה שגוי ומשם הדרך לפתרון היא די קצרה.

אני שוקל ברצינות להפוך את שיטת קליטת הכניסות הזו לשיטה היחידה, אבל בשביל שזה יקרה היא תצטרך להיות יותר ידידותית למשתמש הפשוט:
למשל אם תהליך העיבוד יתבצע אוטומטית פעם ב10 דקות, ואולי אפילו בכל פעם שמנהל האתר בודק את הסטטיסטיקות.
מימוש כזה יגרום לעסק להיות שקוף, תוך הורדה משמעותית מהעומס על השרת.

זה עדיין לא הסוף של מקצה שיפור הביצועים: אני עדיין צריך לשפר את הביצועים של השאילתות, אבל זה בהחלט צעד חשוב בכיוון.