Index: modules/simpletest/tests/session.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v
retrieving revision 1.28
diff -u -r1.28 session.test
--- modules/simpletest/tests/session.test	12 May 2010 08:26:15 -0000	1.28
+++ modules/simpletest/tests/session.test	26 May 2010 13:39:50 -0000
@@ -181,6 +181,48 @@
   }
 
   /**
+   * Test that sessions are only saved when necessary.
+   */
+  function testSessionWrite() {
+    $user = $this->drupalCreateUser(array('access content'));
+    $this->drupalLogin($user);
+
+    $sql = 'SELECT u.access, s.timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
+    $times1 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+
+    // Before every request we sleep one second to make sure that if the session
+    // is saved, its timestamp will change.
+
+    // Modify the session.
+    sleep(1);
+    $this->drupalGet('session-test/set/foo');
+    $times2 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times2->access, $times1->access, t('Users table was not updated.'));
+    $this->assertNotEqual($times2->timestamp, $times1->timestamp, t('Sessions table was updated.'));
+
+    // Write the same value again, i.e. do not modify the session.
+    sleep(1);
+    $this->drupalGet('session-test/set/foo');
+    $times3 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times3->access, $times1->access, t('Users table was not updated.'));
+    $this->assertEqual($times3->timestamp, $times2->timestamp, t('Sessions table was not updated.'));
+
+    // Do not change the session.
+    sleep(1);
+    $this->drupalGet('');
+    $times4 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times4->access, $times3->access, t('Users table was not updated.'));
+    $this->assertEqual($times4->timestamp, $times3->timestamp, t('Sessions table was not updated.'));
+
+    // Force updating of users and sessions table once per second.
+    variable_set('session_write_interval', 0);
+    $this->drupalGet('');
+    $times5 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertNotEqual($times5->access, $times4->access, t('Users table was updated.'));
+    $this->assertNotEqual($times5->timestamp, $times4->timestamp, t('Sessions table was updated.'));
+  }
+
+  /**
    * Reset the cookie file so that it refers to the specified user.
    *
    * @param $uid User id to set as the active session.
Index: includes/session.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/session.inc,v
retrieving revision 1.83
diff -u -r1.83 session.inc
--- includes/session.inc	1 May 2010 08:12:22 -0000	1.83
+++ includes/session.inc	26 May 2010 13:39:50 -0000
@@ -61,12 +61,22 @@
  *
  * @param $sid
  *   Session ID.
+ * @param $return_last
+ *   If TRUE, return the same value as last time this function was called, if
+ *   the session ID in the two calls are identical. This argument is not used
+ *   when called by PHP.
+ *
  * @return
  *   Either an array of the session data, or an empty string, if no data
  *   was found or the user is anonymous.
  */
-function _drupal_session_read($sid) {
+function _drupal_session_read($sid, $return_last = FALSE) {
   global $user, $is_https;
+  static $last_user;
+
+  if ($return_last) {
+    return !empty($last_user->sid) && $last_user->sid == $sid ? $last_user->session : NULL;
+  }
 
   // Write and Close handlers are called after destructing objects
   // since PHP 5.0.5.
@@ -118,6 +128,8 @@
     $user = drupal_anonymous_user($session);
   }
 
+  $last_user = $user;
+
   return $user->session;
 }
 
@@ -134,6 +146,7 @@
  *   Session ID.
  * @param $value
  *   Serialized array of the session data.
+ *
  * @return
  *   This function will always return TRUE.
  */
@@ -145,31 +158,44 @@
     return;
   }
 
-  $fields = array(
-    'uid' => $user->uid,
-    'cache' => isset($user->cache) ? $user->cache : 0,
-    'hostname' => ip_address(),
-    'session' => $value,
-    'timestamp' => REQUEST_TIME,
-  );
-  $key = array('sid' => $sid);
-  if ($is_https) {
-    $key['ssid'] = $sid;
-    $insecure_session_name = substr(session_name(), 1);
-    // The "secure pages" setting allows a site to simultaneously use both
-    // secure and insecure session cookies. If enabled, use the insecure session
-    // identifier as the sid.
-    if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) {
-      $key['sid'] = $_COOKIE[$insecure_session_name];
+  // Check whether $_SESSION has been changed in this request.
+  $is_changed = _drupal_session_read($sid, TRUE) !== $value;
+
+  if ($is_changed) {
+    $fields = array(
+      'uid' => $user->uid,
+      'cache' => isset($user->cache) ? $user->cache : 0,
+      'hostname' => ip_address(),
+      'session' => $value,
+      'timestamp' => REQUEST_TIME,
+    );
+    $key = array('sid' => $sid);
+    if ($is_https) {
+      $key['ssid'] = $sid;
+      $insecure_session_name = substr(session_name(), 1);
+      // The "secure pages" setting allows a site to simultaneously use both
+      // secure and insecure session cookies. If enabled, use the insecure session
+      // identifier as the sid.
+      if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) {
+        $key['sid'] = $_COOKIE[$insecure_session_name];
+      }
     }
+    db_merge('sessions')
+      ->key($key)
+      ->fields($fields)
+      ->execute();
   }
-  db_merge('sessions')
-    ->key($key)
-    ->fields($fields)
-    ->execute();
 
-  // Last access time is updated no more frequently than once every 180 seconds.
-  // This reduces contention in the users table.
+  // For performance reasons, last access time and session timestamp are updated
+  // no more frequently than once every 180 seconds.
+  if (!$is_changed && REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
+    db_update('sessions')
+      ->fields(array(
+        'timestamp' => REQUEST_TIME
+      ))
+      ->condition('sid', $sid)
+      ->execute();
+  }
   if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
     db_update('users')
       ->fields(array(
@@ -183,7 +209,7 @@
 }
 
 /**
- * Initialize the session handler, starting a session if needed.
+ * Initializes the session handler, starting a session if needed.
  */
 function drupal_session_initialize() {
   global $user;
@@ -215,7 +241,7 @@
 }
 
 /**
- * Forcefully start a session, preserving already set session data.
+ * Forcefully starts a session, preserving already set session data.
  *
  * @ingroup php_wrappers
  */
@@ -236,7 +262,7 @@
 }
 
 /**
- * Commit the current session, if necessary.
+ * Commits the current session, if necessary.
  *
  * If an anonymous user already have an empty session, destroy it.
  */
@@ -267,7 +293,7 @@
 }
 
 /**
- * Return whether a session has been started.
+ * Returns whether a session has been started.
  */
 function drupal_session_started($set = NULL) {
   static $session_started = FALSE;
@@ -319,7 +345,7 @@
 /**
  * Session handler assigned by session_set_save_handler().
  *
- * Cleanup a specific session.
+ * Cleans up a specific session.
  *
  * @param $sid
  *   Session ID.
@@ -361,7 +387,7 @@
 }
 
 /**
- * End a specific user's session(s).
+ * Ends a specific user's session(s).
  *
  * @param $uid
  *   User ID.
@@ -375,7 +401,7 @@
 /**
  * Session handler assigned by session_set_save_handler().
  *
- * Cleanup stalled sessions.
+ * Cleans up stalled sessions.
  *
  * @param $lifetime
  *   The value of session.gc_maxlifetime, passed by PHP.
@@ -394,7 +420,7 @@
 }
 
 /**
- * Determine whether to save session data of the current request.
+ * Determines whether to save session data of the current request.
  *
  * This function allows the caller to temporarily disable writing of
  * session data, should the request end while performing potentially
@@ -404,6 +430,7 @@
  * @param $status
  *   Disables writing of session data when FALSE, (re-)enables
  *   writing when TRUE.
+ *
  * @return
  *   FALSE if writing session data has been disabled. Otherwise, TRUE.
  */
