From 905a1f183c7e65a6f192a4dcf6315f53383737e1 Mon Sep 17 00:00:00 2001
From: Sujith Kakarlapudi <sujith.kakarlapudi@sjsu.edu>
Date: Tue, 15 Apr 2025 08:22:38 -0700
Subject: [PATCH] Generate test data for Impressions with random but sequential
 timestamps to replicate real-time behavior

---
 src/models/GroupModel.php      | 32 +++++++++++------
 src/models/ImpressionModel.php | 29 ++++++++++-----
 tests/ManyUserExperiment.php   | 66 ++++++++++++++++++++++++++++------
 3 files changed, 98 insertions(+), 29 deletions(-)

diff --git a/src/models/GroupModel.php b/src/models/GroupModel.php
index 391ec4f5f..497950c3e 100644
--- a/src/models/GroupModel.php
+++ b/src/models/GroupModel.php
@@ -266,12 +266,16 @@ class GroupModel extends Model implements MediaConstants
      * @param int $render_engine what parser to use when drawing wiki pages or
      *      posts (mediawiki == C\MEDIAWIKI_ENGINE ,
      *      markdown == C\MARKDOWN_ENGINE)
+     * @param bool $skip_impression whether to skip impression logging or not
+     * @param int $input_timestamp optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      * @return int id of group added
      */
     public function addGroup($group_name, $user_id,
         $register = C\REQUEST_JOIN, $member = C\GROUP_READ,
         $voting = C\NON_VOTING_GROUP, $post_lifetime = C\FOREVER,
-        $encryption = 0, $render_engine = C\MEDIAWIKI_ENGINE)
+        $encryption = 0, $render_engine = C\MEDIAWIKI_ENGINE,
+        $skip_impression = false, $input_timestamp = -1)
     {
         $db = $this->db;
         $private_db = $this->private_db;
@@ -294,10 +298,12 @@ class GroupModel extends Model implements MediaConstants
             JOIN_DATE) VALUES
             ($user_id, $last_id, " . C\ACTIVE_STATUS . ", $now)";
         $db->execute($sql);
-        ImpressionModel::initWithDb($user_id, $last_id, C\GROUP_IMPRESSION,
-            $db);
-        ImpressionModel::initWithDb(C\PUBLIC_GROUP_ID, $last_id,
-            C\GROUP_IMPRESSION, $db);
+        if ($skip_impression === false) {
+            ImpressionModel::initWithDb($user_id, $last_id, C\GROUP_IMPRESSION,
+                $db, $input_timestamp);
+            ImpressionModel::initWithDb(C\PUBLIC_GROUP_ID, $last_id,
+                C\GROUP_IMPRESSION, $db, $input_timestamp);
+        }
         if ($encryption != 0) {
             $sql = "INSERT INTO TYPE_KEYS (TYPE_ID, KEY_NAME) VALUES (?, ?)";
             //AES 256 is 32 bytes long (8*32 bits)
@@ -993,11 +999,13 @@ class GroupModel extends Model implements MediaConstants
      * @param int $ups number of times the item has been upvoted
      * @param int $downs number of times the item has been down voted
      * @param int $flag number of times the item has been flagged
+     * @param int $input_timestamp optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      * @return int $id of item added
      */
     public function addGroupItem($parent_id, $group_id, $user_id, $title,
         $description, $type= C\STANDARD_GROUP_ITEM, $post_time = 0, $url = "",
-        $edit_time = 0, $ups = 0, $downs = 0, $flag = 0)
+        $edit_time = 0, $ups = 0, $downs = 0, $flag = 0, $input_timestamp = -1)
     {
         $db = $this->db;
         if ($post_time == 0) {
@@ -1021,7 +1029,7 @@ class GroupModel extends Model implements MediaConstants
             $sql = "UPDATE GROUP_ITEM SET PARENT_ID=? WHERE ID=?";
             $db->execute($sql, [$id, $id]);
             ImpressionModel::initWithDb($user_id, $id, C\THREAD_IMPRESSION,
-                $db);
+                $db, $input_timestamp);
         }
         return $id;
     }
@@ -1883,12 +1891,14 @@ class GroupModel extends Model implements MediaConstants
      *      configs/TokenTool.php)
      * @param int $pubdate if not >=0 then the tiemstate of when the post was
      *      published; otherwise current time will be used
+     * @param int $input_timestamp optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      * @return int $page_id id of added or updated page
      */
     public function setPageName($user_id, $group_id, $page_name, $page,
         $locale_tag, $edit_comment, $thread_title, $thread_description,
         $base_address = "", $additional_substitutions = [],
-        $add_relationship_data = true, $pubdate = -1)
+        $add_relationship_data = true, $pubdate = -1, $input_timestamp = -1)
     {
         $db = $this->db;
         $pubdate = ($pubdate == -1) ? time() : $pubdate;
@@ -1934,16 +1944,16 @@ class GroupModel extends Model implements MediaConstants
         } else {
             $discuss_thread = $this->addGroupItem(0, $group_id, $user_id,
                 $thread_title, $thread_description . " " . date("r", $pubdate),
-                C\WIKI_GROUP_ITEM);
+                C\WIKI_GROUP_ITEM, input_timestamp: $input_timestamp);
             $sql = "INSERT INTO GROUP_PAGE (DISCUSS_THREAD, GROUP_ID, TITLE,
                 PAGE, LOCALE_TAG, LAST_MODIFIED) VALUES (?, ?, ?, ?, ?, ?)";
             $result = $db->execute($sql, [$discuss_thread, $group_id,
                 $page_name, $parsed_page, $locale_tag, $pubdate]);
             $page_id = $db->insertID("GROUP_PAGE");
             ImpressionModel::initWithDb($user_id, $page_id, C\WIKI_IMPRESSION,
-                $db);
+                $db, $input_timestamp);
             ImpressionModel::initWithDb(C\PUBLIC_USER_ID, $page_id,
-                C\WIKI_IMPRESSION, $db);
+                C\WIKI_IMPRESSION, $db, $input_timestamp);
         }
         $sql = "INSERT INTO GROUP_PAGE_HISTORY (PAGE_ID, EDITOR_ID,
             GROUP_ID, TITLE, PAGE, LOCALE_TAG, PUBDATE, EDIT_COMMENT)
diff --git a/src/models/ImpressionModel.php b/src/models/ImpressionModel.php
index 3807e959c..296d43460 100644
--- a/src/models/ImpressionModel.php
+++ b/src/models/ImpressionModel.php
@@ -78,10 +78,13 @@ class ImpressionModel extends Model
      *      information of
      * @param int $type_id type of particular item we are adding analytic
      *      information of (group, wiki, thread, etc)
+     * @param int $time optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      */
-    public function add($user_id, $item_id, $type_id)
+    public function add($user_id, $item_id, $type_id, $time = -1)
     {
-        $this->addWithDb($user_id, $item_id, $type_id, $this->db);
+        $time = ($time > 0) ? $time : time();
+        $this->addWithDb($user_id, $item_id, $type_id, $this->db, $time);
     }
     /**
      * Used to add a count record related to a web search to the impression
@@ -547,8 +550,11 @@ class ImpressionModel extends Model
      *      information of (group, wiki, thread, etc)
      * @param object $db a DatasourceManager used to query the Yioop
      *      Yioop database
+     * @param int $time optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      */
-    public static function initWithDb($user_id, $item_id, $type_id, $db)
+    public static function initWithDb($user_id, $item_id, $type_id, $db,
+        $time = -1)
     {
         $dbinfo = ["DBMS" => C\DBMS, "DB_HOST" => C\DB_HOST,
             "DB_USER" => C\DB_USER, "DB_PASSWORD" => C\DB_PASSWORD,
@@ -557,7 +563,8 @@ class ImpressionModel extends Model
             (?, ?, ?, " . C\FOREVER .", 0, 0, -1, -1)";
         $sql = $db->insertIgnore($sql, $dbinfo);
         $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id]);
-        self::addWithDb($user_id, $item_id, $type_id, $db);
+        $time = ($time > 0) ? $time : time();
+        self::addWithDb($user_id, $item_id, $type_id, $db, $time);
     }
     /**
      * Used to add a count record related to a particular user for a particular
@@ -574,21 +581,27 @@ class ImpressionModel extends Model
      *      information of (group, wiki, thread, etc)
      * @param object $db a DatasourceManager used to query the Yioop
      *      Yioop database
+     * @param int $input_timestamp optional UNIX timestamp to use for impression
+            tracking; if set to -1 or omitted, the current time will be used
      */
-    public static function addWithDb($user_id, $item_id, $type_id, $db)
+    public static function addWithDb($user_id, $item_id, $type_id, $db,
+        $input_timestamp = -1)
     {
         if (!C\GROUP_ANALYTICS_MODE || C\GROUP_ANALYTICS_MODE == "0" ||
             !is_numeric($user_id) || !is_numeric($item_id)) {
             return;
         }
+        $input_timestamp = ($input_timestamp > 0) ? $input_timestamp : time();
         $dbinfo = ["DBMS" => C\DBMS, "DB_HOST" => C\DB_HOST,
             "DB_USER" => C\DB_USER, "DB_PASSWORD" => C\DB_PASSWORD,
             "DB_NAME" => C\DB_NAME];
         $sql = "INSERT INTO ITEM_IMPRESSION VALUES (?, ?, ?, ?)";
         if ($user_id != C\PUBLIC_USER_ID) {
-            $db->execute($sql, [$user_id, $item_id, $type_id, time()]);
+            $db->execute($sql, [$user_id, $item_id, $type_id,
+                $input_timestamp]);
         }
-        $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id, time()]);
+        $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id,
+            $input_timestamp]);
         $sql = "DELETE FROM ITEM_IMPRESSION_SUMMARY
             WHERE USER_ID=? AND ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD = ".
             C\MOST_RECENT_VIEW;
@@ -596,7 +609,7 @@ class ImpressionModel extends Model
         $sql = "INSERT INTO ITEM_IMPRESSION_SUMMARY VALUES
             (?, ?, ?, " . C\MOST_RECENT_VIEW .", ?, 0, -1, -1)";
         $sql = $db->insertIgnore($sql, $dbinfo);
-        $db->execute($sql, [$user_id, $item_id, $type_id, time()]);
+        $db->execute($sql, [$user_id, $item_id, $type_id, $input_timestamp]);
         $sql = "UPDATE ITEM_IMPRESSION_SUMMARY
             SET NUM_VIEWS = NUM_VIEWS + 1 WHERE USER_ID=? AND
             ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD = ". C\FOREVER . "
diff --git a/tests/ManyUserExperiment.php b/tests/ManyUserExperiment.php
index cbd1e5ead..6a8ce3c1d 100644
--- a/tests/ManyUserExperiment.php
+++ b/tests/ManyUserExperiment.php
@@ -65,6 +65,43 @@ C\nsconddefine("ME_NUM_THREADS", 100);
 C\nsconddefine("ME_NUM_ROLES", 100);
 C\nsconddefine("ME_NUM_MIXES", 100);
 C\nsconddefine("ME_NUM_RESOURCES_IMPRESSION", 15);
+C\nsconddefine("USE_RANDOM_TIMESTAMP", true);
+C\nsconddefine('ONE_MINUTE_SECONDS', 60);
+C\nsconddefine('ONE_HOUR_SECONDS', 3600);
+C\nsconddefine('ONE_MONTH_SECONDS', 2592000);
+C\nsconddefine('ONE_YEAR_SECONDS', 31536000);
+C\nsconddefine('TWO_YEARS_SECONDS', 2 * 31536000);
+C\nsconddefine('THREE_YEARS_SECONDS', 3 * 31536000);
+C\nsconddefine('FOUR_YEARS_SECONDS', 4 * 31536000);
+C\nsconddefine('FIVE_YEARS_SECONDS', 5 * 31536000);
+C\nsconddefine('NUMBER_OF_IMPRESSIONS', 20000);
+
+function getNextTimestamp() {
+    static $previous_timestamp = null;
+    $current_time = time();
+    $target_count = C\NUMBER_OF_IMPRESSIONS;
+    if ($previous_timestamp === null) {
+        $start_min = $current_time - C\FIVE_YEARS_SECONDS;
+        $start_max = $current_time - C\THREE_YEARS_SECONDS;
+        $previous_timestamp = rand($start_min, $start_max);
+    }
+    $remaining_time = $current_time - $previous_timestamp;
+    if ($remaining_time < C\ONE_HOUR_SECONDS) {
+        return null;
+    }
+    $average_gap = $remaining_time / $target_count;
+    $min_gap = max(C\ONE_HOUR_SECONDS, (int)($average_gap / 2));
+    $max_gap = min(C\ONE_MONTH_SECONDS, (int)($average_gap * 2));
+    if ($min_gap > $max_gap) {
+        $min_gap = $max_gap;
+    }
+    $increment = rand($min_gap, $max_gap);
+    $previous_timestamp += $increment;
+    if ($previous_timestamp > $current_time) {
+        $previous_timestamp = $current_time;
+    }
+    return $previous_timestamp;
+}
 $user_model = new M\UserModel();
 $group_model = new M\GroupModel();
 //Add lots of users
@@ -79,9 +116,10 @@ for ($i = 0; $i < C\ME_NUM_USERS; $i++) {
     }
     $personal_group_name = C\PERSONAL_GROUP_PREFIX . $id;
     if ($group_model->getGroupId($personal_group_name) < 0) {
-        $group_model->addGroup($personal_group_name,
-            $id, C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
-            C\NON_VOTING_GROUP, C\FOREVER, 0);
+        $input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
+        $group_model->addGroup($personal_group_name, $id, C\INVITE_ONLY_JOIN,
+            C\ACTIVE_STATUS, C\NON_VOTING_GROUP, C\FOREVER, 0,
+            skip_impression: true, input_timestamp: $input_timestamp);
     }
     $user_ids[$i] = $id;
 }
@@ -89,8 +127,10 @@ for ($i = 0; $i < C\ME_NUM_USERS; $i++) {
 $group_ids = [];
 for ($i = 0; $i < C\ME_NUM_GROUPS; $i++) {
     echo "Creating Group $i\n";
-    $group_ids[$i] = $group_model->addGroup("Group$i", $user_ids[$i], ["User$i"],
-        C\PUBLIC_JOIN, C\GROUP_READ_WIKI);
+    $input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
+    $group_ids[$i] = $group_model->addGroup("Group$i", $user_ids[$i],
+        C\PUBLIC_JOIN, C\GROUP_READ_WIKI, skip_impression : true,
+        input_timestamp: $input_timestamp);
 }
 // add lots of users to groups (everyone belongs to at least group 1)
 for ($i = 0; $i < C\ME_NUM_GROUPS; $i++) {
@@ -110,15 +150,18 @@ for ($i = 0; $i < C\ME_NUM_THREADS; $i++) {
     $user_id = $user_ids[$i];
     $title = "My first thread: User $user_id";
     echo "Adding User $user_id  Group {$group_ids[1]} Thread\n";
+    $input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
     $parent_id = $group_model->addGroupItem(0, $group_ids[1], $user_id,
-        $title, "I really like this thread, User:$user_id");
+        $title, "I really like this thread, User:$user_id",
+        input_timestamp: $input_timestamp);
     $title = "--" . $title;
     for ($j = 0; $j < ($i / 10) + 1; $j++) {
         $comment_user_id = $user_ids[$j];
+        $input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
         $impression_model->add($comment_user_id, $parent_id,
-            C\THREAD_IMPRESSION);
+            C\THREAD_IMPRESSION, time: $input_timestamp);
         $impression_model->add($comment_user_id, $group_ids[1],
-            C\GROUP_IMPRESSION);
+            C\GROUP_IMPRESSION, time: $input_timestamp);
         $group_model->addGroupItem($parent_id, $group_ids[1], $comment_user_id,
             $title, "I totally agree with you, User:$comment_user_id");
     }
@@ -178,9 +221,11 @@ EOD;
 $page_content = str_replace("'", "&#039;", $page);
 $page_name = "Test";
 echo "Adding wiki page $page_name to group $group_ids[0]...\n";
+$input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
 $page_id = $group_model->setPageName($user_ids[0], $group_ids[0], $page_name,
     $page_content, "en-US", "Creating test wiki page",
-    "{$page_name} Wiki Page Created!", "Discuss the page in this thread!");
+    "{$page_name} Wiki Page Created!", "Discuss the page in this thread!",
+    input_timestamp: $input_timestamp);
 echo "Added the wiki page...\n";
 echo "Creating movies folder and adding dummy mp4 resources\n";
 $resources = [];
@@ -378,7 +423,8 @@ for ($i = 0; $i < C\ME_NUM_USERS; $i++) {
             " $resource_name\n";
         $resource_id = unpack('n', md5($group_ids[0] . $page_id .
             $folders_path[$resource_type][1] . "/" . $resource_name, true))[1];
+        $input_timestamp = C\USE_RANDOM_TIMESTAMP ? getNextTimestamp() : -1;
         $impression_model->add($user_id, $resource_id,
-            C\RESOURCE_IMPRESSION);
+            C\RESOURCE_IMPRESSION, time: getNextTimestamp());
     }
 }
-- 
2.46.0.windows.1

