Index: media.api.php
===================================================================
RCS file: /cvs/drupal/contributions/modules/media/media.api.php,v
retrieving revision 1.6
diff -u -p -r1.6 media.api.php
--- media.api.php	1 Feb 2011 17:44:42 -0000	1.6
+++ media.api.php	11 Feb 2011 22:35:54 -0000
@@ -83,4 +83,14 @@ function hook_media_browser_plugin_info(
  */
 function media_media_operations() {
   
-}
\ No newline at end of file
+}
+
+/**
+ * Allows modules to flush caches containing rendered media.
+ *
+ * @param $fid
+ *   The fid of the media being updated or deleted. When FALSE, the hook is
+ *   being triggered by a change that affects all rendered media.
+ */
+function hook_media_rendered_flush($fid = FALSE) {
+}
Index: media.fields.inc
===================================================================
RCS file: /cvs/drupal/contributions/modules/media/media.fields.inc,v
retrieving revision 1.25
diff -u -p -r1.25 media.fields.inc
--- media.fields.inc	14 Jan 2011 23:30:22 -0000	1.25
+++ media.fields.inc	11 Feb 2011 22:35:54 -0000
@@ -450,8 +450,9 @@ function media_field_delete_revision($en
  * Implements hook_field_instance_update().
  */
 function media_field_update_instance($instance, $prior_instance) {
-  // Clear the filter cache when updating instance settings for a media entity.
+  // When field instance settings for a media type are changed, flush caches
+  // that contain the rendered output of media entities.
   if ($instance['entity_type'] == 'media') {
-    media_filter_invalidate_caches();
+    media_rendered_flush();
   }
 }
Index: media.filter.inc
===================================================================
RCS file: /cvs/drupal/contributions/modules/media/media.filter.inc,v
retrieving revision 1.17
diff -u -p -r1.17 media.filter.inc
--- media.filter.inc	1 Feb 2011 17:44:42 -0000	1.17
+++ media.filter.inc	11 Feb 2011 22:35:54 -0000
@@ -146,8 +146,8 @@ function media_token_to_markup($match, $
      throw new Exception('Could not load media object');
     }
 
-    // Track the fid of this media object in the {media_filter_usage} table.
-    media_filter_track_usage($media['fid']);
+    // Update the {media_filter_usage} table.
+    media_filter_usage_update($media['fid']);
 
     $settings = is_array($media['attributes']) ? $media['attributes'] : array();
     $attribute_whitelist = media_variable_get('wysiwyg_allowed_attributes');
@@ -340,8 +340,6 @@ function media_format_form($form, $form_
   return $form;
 }
 
-
-
 /**
  * Wrapper around field_view_field, returns the file field w/o a label
  *
@@ -362,65 +360,100 @@ function media_get_file_without_label($m
 }
 
 /**
- * Clears caches that may be affected by the media filter.
+ * Tracks that the filter cache contains the rendered output of this media entity.
+ *
+ * The media filter maps tokens such as
+ * @code [[{"fid":"42", "view_mode":"media_large"}]] @endcode
+ * to the HTML of the media entity rendered in the view mode. By default,
+ * Drupal's filter system caches the filtered output of formatted text, so when
+ * a media entity or the configuration of a view mode is updated, that cache
+ * must be flushed.
  *
- * The media filter calls media_load(). This means that if a media object
- * is updated, the check_markup() and field caches could return stale content.
  * There are several possible approaches to deal with this:
- *  - Disable filter caching in media_filter_info(), this was found to cause a
- *    30% performance hit from profiling four node teasers, due to both the
- *    media filter itself, and other filters that can't be cached.
- *  - Clear the filter and field caches whenever any media node is updated, this
- *    would ensure cache coherency but would reduce the effectiveness of those
- *    caches on high traffic sites with lots of media content updates.
- *  - The approach taken here: Record the fid of all media objects that are
- *    referenced by the media filter. Only clear the filter and field caches
- *    when one of these is updated, as opposed to all media objects.
- *  - @todo: consider an EntityFieldQuery to limit cache clearing to only those
- *    entities that use a text format with the media filter, possibly checking
- *    the contents of those fields to further limit this to fields referencing
- *    the media object being updated. This would need to be implemented
- *    carefully to avoid scalability issues with large result sets, and may
- *    not be worth the effort.
+ *  - Disable filter caching in media_filter_info(). This was found to slow down
+ *    page response time by 30% for a page with four node teasers. See
+ *    hook_filter_info() for more details about the filter cache.
+ *  - Clear the filter cache whenever any media entity is updated. This would
+ *    ensure cache coherency but would reduce the effectiveness of those caches
+ *    on high traffic sites with frequent media updates.
+ *  - When a media entity is updated, delete only the filter cache records that
+ *    include tokens for that entity. A solution for how to do this has not yet
+ *    been worked out.
+ *
+ * The approach currently used by the Media module is to record the fid of all
+ * media entities that are referenced by any cached filter record, and flush the
+ * entire filter cache when one of these entities is updated. When a media 
+ * entity is not referenced by any filter cache record, updates to that entity 
+ * will not trigger a cache flush.
  *
  * @param $fid
- *   Optional media fid being updated. If not given, the cache will be cleared
- *   as long as any file is referenced.
+ *   The media fid.
+ *
+ * @see media_filter_usage_prune()
  */
-function media_filter_invalidate_caches($fid = FALSE) {
-  // If fid is passed, confirm that it has previously been referenced by the
-  // media filter. If not, clear the cache if the {media_filter_usage} has any
-  // valid records.
-  if (($fid && db_query('SELECT fid FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField()) || (!$fid && media_filter_usage_has_records())) {
-    // @todo: support entity cache, either via a hook, or using module_exists().
-    cache_clear_all('*', 'cache_filter', TRUE);
-    cache_clear_all('*', 'cache_field', TRUE);
+function media_filter_usage_update($fid) {
+  // It would be impractical to check when formatted text is edited to remove
+  // references to fids, but we don't want stale usage records never being
+  // deleted, so we track a timestamp, and allow usage records to expire. The
+  // timestamp doesn't need to be very precise, so avoid unnecessarily frequent
+  // UPDATE statements.
+  $timestamp = db_query('SELECT timestamp FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField();
+  if (!$timestamp || $timestamp <= REQUEST_TIME - 86400 * 30) {
+    db_merge('media_filter_usage')->key(array('fid' => $fid))->fields(array('fid' => $fid, 'timestamp' => REQUEST_TIME))->execute();
   }
 }
 
 /**
- * Determines if the {media_filter_usage} table has any entries.
+ * Removes an entry or all entries from the {media_filter_usage} table.
+ *
+ * @param $fid
+ *   The media fid. If FALSE, all entries are deleted.
+ *
+ * @see media_filter_usage_update()
  */
-function media_filter_usage_has_records() {
-  return  (bool) db_query_range('SELECT 1 FROM {media_filter_usage} WHERE fid > :fid', 0, 1, array(':fid' => 0))->fetchField();
+function media_filter_usage_delete($fid = FALSE) {
+  if ($fid) {
+    db_delete('media_filter_usage')->condition('fid', $fid)->execute();
+  }
+  else {
+    db_truncate('media_filter_usage')->execute();
+  }
+}
+
+/**
+ * Garbage collects old entries from the {media_filter_usage} table.
+ *
+ * @see media_filter_usage_update()
+ */
+function media_filter_usage_prune() {
+  // If a media fid's last recorded usage was more than 4 months ago, remove it
+  // from the {media_filter_usage} table. This prevents fids from staying in
+  // this table indefinitely, even if the text that references them is edited
+  // or deleted.
+  // @todo: The 4 month period corresponds to at least 3 months since the fid
+  //   passed through this module's filter (since media_filter_usage_update()
+  //   only updates each fid's record at most once per month). However, it's
+  //   possible that a fid is still being used on the site, but that the text
+  //   which references it has remained in the field or filter cache for more
+  //   than 3 months. In that case, deleting it here means that if the media
+  //   entity is later edited, media_rendered_flush() won't properly clear the
+  //   field and filter caches and the text which references this fid might not
+  //   be properly updated.
+  db_delete('media_filter_usage')->condition('timestamp', REQUEST_TIME - 86400 * 120, '<')->execute();
 }
 
 /**
- * Tracks usage of media fids by the media filter.
+ * Determines if the {media_filter_usage} table has any entries.
  *
  * @param $fid
- *   The media fid.
+ *   The media fid. If a valid fid, returns TRUE if the there's a usage record
+ *   for that fid. If FALSE, returns if there's a usage record at all.
  */
-function media_filter_track_usage($fid) {
-  // This function only tracks when fids are found by the media filter.
-  // It would be impractical to check when formatted text is edited to remove
-  // references to fids, however by keeping a timestamp, we can implement
-  // rudimentary garbage collection in hook_flush_caches().
-  // However we only need to track that an fid has ever been referenced,
-  // not every time, so avoid updating this table more than once per month,
-  // per fid.
-  $timestamp = db_query('SELECT timestamp FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField();
-  if (!$timestamp || $timestamp <= REQUEST_TIME - 86400 * 30) {
-    db_merge('media_filter_usage')->key(array('fid' => $fid))->fields(array('fid' => $fid, 'timestamp' => REQUEST_TIME))->execute();
+function media_filter_usage_has_records($fid = FALSE) {
+  if ($fid) {
+    return (bool) db_query('SELECT fid FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField();
+  }
+  else {
+    return (bool) db_query_range('SELECT 1 FROM {media_filter_usage} WHERE fid > :fid', 0, 1, array(':fid' => 0))->fetchField();
   }
 }
Index: media.module
===================================================================
RCS file: /cvs/drupal/contributions/modules/media/media.module,v
retrieving revision 1.70
diff -u -p -r1.70 media.module
--- media.module	1 Feb 2011 17:44:42 -0000	1.70
+++ media.module	11 Feb 2011 22:35:54 -0000
@@ -371,7 +371,7 @@ function media_file_delete($file) {
   // @todo: call field_attach_delete() from here, or add media_delete().
 
   // Remove this file from the {media_filter_usage} table.
-  db_delete('media_filter_usage')->condition('fid', $file->fid)->execute();
+  media_filter_usage_delete($file->fid);
 }
 
 /**
@@ -384,8 +384,7 @@ function media_file_delete($file) {
  * @see media_file_style_flush()
  */
 function media_image_style_flush($style) {
-  // When a image style is flushed, clear the filter and field caches.
-  media_filter_invalidate_caches();
+  media_rendered_flush();
 }
 
 /**
@@ -396,8 +395,7 @@ function media_image_style_flush($style)
  * @see media_styles_style_flush()
  */
 function media_file_style_flush($style) {
-  // When a file style is flushed, clear the filter and field caches.
-  media_filter_invalidate_caches();
+  media_rendered_flush();
 }
 
 /**
@@ -408,8 +406,7 @@ function media_file_style_flush($style) 
  * @see media_file_style_flush()
  */
 function media_styles_style_flush($style) {
-  // When a style is flushed, clear the filter and field caches.
-  media_filter_invalidate_caches();
+  media_rendered_flush();
 }
 
 /**
@@ -585,7 +582,7 @@ function media_save($media) {
   }
   else {
     field_attach_update('media', $media);
-    media_filter_invalidate_caches($media->fid);
+    media_rendered_flush($media->fid);
   }
 
   // Since the Media module can't make files into fieldable entities directly
@@ -951,22 +948,18 @@ function media_save_attached_file($file)
       $media->file[LANGUAGE_NONE] = array();
       $media->file[LANGUAGE_NONE][0] = (array)$file + $defaults;
       $media->is_new = TRUE;
+      // @todo Why aren't we calling media_save()? Since we aren't, we have to
+      // duplicate some of its code.
       field_attach_presave('media', $media);
       field_attach_update('media', $media);
       //field_attach_insert('media', $media);
-      // @todo Why aren't we calling media_save() instead of the above code.
-      //   Since we aren't, we have to update usage here too, as we do in
-      //   media_save().
       file_usage_delete((object)(array) $media, 'file', 'media', $media->fid);
-
-      // Also clear cached text formats from here.
-      media_filter_invalidate_caches($media->fid);
+      media_rendered_flush($media->fid);
     }
   }
   return $media;
 }
 
-
 /**
  * Implements hook_element_info().
  */
@@ -1108,14 +1101,14 @@ function media_element_validate(&$elemen
 /**
  * Implements hook_filter_info().
  *
- * For performance, the media filter is allowed to be cached by default. See
- * media_filter_invalidate_caches() for details. Some sites may use advanced
- * media styles with rendering implentations that differs per theme or based on
- * other runtime information. For these sites, it may be necessary to implement
- * a module with a hook_filter_info_alter() implementation that sets
- * $info['media_filter']['cache'] to FALSE.
+ * For performance, the media filter is allowed to be cached by default. Some
+ * sites may use advanced media styles with rendering implentations that differs
+ * per theme or based on other runtime information. For these sites, it may be
+ * necessary to implement a module with a hook_filter_info_alter()
+ * implementation that sets $info['media_filter']['cache'] to FALSE.
  *
- * @see media_filter_invalidate_caches()
+ * @see media_rendered_flush()
+ * @see media_filter_usage_update()
  */
 function media_filter_info() {
   $filters['media_filter'] = array(
@@ -1220,15 +1213,32 @@ function media_file_validate_types(stdCl
 }
 
 /**
+ * Clears caches that contain the HTML of rendered media.
+ *
+ * @param $fid
+ *   Optional media fid to allow selective clearing of caches.
+ */
+function media_rendered_flush($fid = FALSE) {
+  // If there is any filter cache record that references this media entity,
+  // flush the entire filter cache.
+  // @see media_filter_usage_update()
+  if (media_filter_usage_has_records($fid)) {
+    cache_clear_all('*', 'cache_filter', TRUE);
+    // The Field API caches filtered text in the field cache, so clear that too.
+    // @todo Consider an EntityFieldQuery to limit this to entities that use a
+    // text format that includes the media filter.
+    cache_clear_all('*', 'cache_field', TRUE);
+  }
+
+  // Allow other modules to flush additional caches. For example, a module that
+  // uses the drupal_render() cache to cache the display of content containing
+  // media entities may use this hook to clear those cache entries.
+  module_invoke_all('media_rendered_flush', $fid);
+}
+
+/**
  * Implements hook_flush_caches().
  */
 function media_flush_caches() {
-  // Garbage collection for the {media_filter_usage} table. If an fid was last
-  // recorded four months ago (minimum three months due to logic in
-  // media_filter_track_usage()), remove it from this table while the filter
-  // and field caches are being cleared. If the fid is still in use, it will
-  // be added back to the table the next time check_markup() runs on that
-  // content. This prevents fids from staying in this table indefinitely,
-  // even if the post that references them is edited or deleted.
-  db_delete('media_filter_usage')->condition('timestamp', REQUEST_TIME - 86400 * 120, '<')->execute();
+  media_filter_usage_prune();
 }
