| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "content/browser/media/media_internals.h" | 5 #include "content/browser/media/media_internals.h" |
| 6 | 6 |
| 7 #include "base/metrics/histogram.h" | 7 #include "base/metrics/histogram.h" |
| 8 #include "base/strings/string16.h" | 8 #include "base/strings/string16.h" |
| 9 #include "base/strings/string_number_conversions.h" | 9 #include "base/strings/string_number_conversions.h" |
| 10 #include "base/strings/stringprintf.h" | 10 #include "base/strings/stringprintf.h" |
| 11 #include "content/public/browser/browser_thread.h" | 11 #include "content/public/browser/browser_thread.h" |
| 12 #include "content/public/browser/notification_observer.h" | |
| 13 #include "content/public/browser/notification_registrar.h" | |
| 14 #include "content/public/browser/notification_service.h" | 12 #include "content/public/browser/notification_service.h" |
| 15 #include "content/public/browser/notification_types.h" | 13 #include "content/public/browser/notification_types.h" |
| 16 #include "content/public/browser/render_frame_host.h" | 14 #include "content/public/browser/render_frame_host.h" |
| 17 #include "content/public/browser/render_process_host.h" | 15 #include "content/public/browser/render_process_host.h" |
| 18 #include "content/public/browser/web_contents.h" | 16 #include "content/public/browser/web_contents.h" |
| 19 #include "content/public/browser/web_ui.h" | 17 #include "content/public/browser/web_ui.h" |
| 20 #include "media/audio/audio_parameters.h" | 18 #include "media/audio/audio_parameters.h" |
| 21 #include "media/base/media_log_event.h" | 19 #include "media/base/media_log_event.h" |
| 22 #include "media/filters/decrypting_video_decoder.h" | 20 #include "media/filters/decrypting_video_decoder.h" |
| 23 #include "media/filters/gpu_video_decoder.h" | 21 #include "media/filters/gpu_video_decoder.h" |
| (...skipping 218 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 242 kAudioLogUpdateFunction, &dict); | 240 kAudioLogUpdateFunction, &dict); |
| 243 } | 241 } |
| 244 | 242 |
| 245 void AudioLogImpl::StoreComponentMetadata(int component_id, | 243 void AudioLogImpl::StoreComponentMetadata(int component_id, |
| 246 base::DictionaryValue* dict) { | 244 base::DictionaryValue* dict) { |
| 247 dict->SetInteger("owner_id", owner_id_); | 245 dict->SetInteger("owner_id", owner_id_); |
| 248 dict->SetInteger("component_id", component_id); | 246 dict->SetInteger("component_id", component_id); |
| 249 dict->SetInteger("component_type", component_); | 247 dict->SetInteger("component_type", component_); |
| 250 } | 248 } |
| 251 | 249 |
| 252 class MediaInternals::MediaInternalsUMAHandler : public NotificationObserver { | 250 class MediaInternals::MediaInternalsUMAHandler { |
| 253 public: | 251 public: |
| 254 MediaInternalsUMAHandler(); | 252 MediaInternalsUMAHandler(); |
| 255 | 253 |
| 256 // NotificationObserver implementation. | 254 // Called when a render process is terminated. Reports the pipeline status to |
| 257 void Observe(int type, | 255 // UMA for every player associated with the renderer process and then deletes |
| 258 const NotificationSource& source, | 256 // the player state. |
| 259 const NotificationDetails& details) override; | 257 void OnProcessTerminated(int render_process_id); |
| 260 | |
| 261 // Reports the pipeline status to UMA for every player | |
| 262 // associated with the renderer process and then deletes the player state. | |
| 263 void LogAndClearPlayersInRenderer(int render_process_id); | |
| 264 | 258 |
| 265 // Helper function to save the event payload to RendererPlayerMap. | 259 // Helper function to save the event payload to RendererPlayerMap. |
| 266 void SavePlayerState(const media::MediaLogEvent& event, | 260 void SavePlayerState(int render_process_id, |
| 267 int render_process_id); | 261 const media::MediaLogEvent& event); |
| 268 | 262 |
| 269 private: | 263 private: |
| 270 struct PipelineInfo { | 264 struct PipelineInfo { |
| 271 media::PipelineStatus last_pipeline_status; | 265 media::PipelineStatus last_pipeline_status; |
| 272 bool has_audio; | 266 bool has_audio; |
| 273 bool has_video; | 267 bool has_video; |
| 274 bool video_dds; | 268 bool video_dds; |
| 275 bool video_decoder_changed; | 269 bool video_decoder_changed; |
| 276 std::string audio_codec_name; | 270 std::string audio_codec_name; |
| 277 std::string video_codec_name; | 271 std::string video_codec_name; |
| (...skipping 14 matching lines...) Expand all Loading... |
| 292 | 286 |
| 293 // Key is playerid | 287 // Key is playerid |
| 294 typedef std::map<int, PipelineInfo> PlayerInfoMap; | 288 typedef std::map<int, PipelineInfo> PlayerInfoMap; |
| 295 | 289 |
| 296 // Key is renderer id | 290 // Key is renderer id |
| 297 typedef std::map<int, PlayerInfoMap> RendererPlayerMap; | 291 typedef std::map<int, PlayerInfoMap> RendererPlayerMap; |
| 298 | 292 |
| 299 // Stores player information per renderer | 293 // Stores player information per renderer |
| 300 RendererPlayerMap renderer_info_; | 294 RendererPlayerMap renderer_info_; |
| 301 | 295 |
| 302 NotificationRegistrar registrar_; | |
| 303 | |
| 304 DISALLOW_COPY_AND_ASSIGN(MediaInternalsUMAHandler); | 296 DISALLOW_COPY_AND_ASSIGN(MediaInternalsUMAHandler); |
| 305 }; | 297 }; |
| 306 | 298 |
| 307 MediaInternals::MediaInternalsUMAHandler::MediaInternalsUMAHandler() { | 299 MediaInternals::MediaInternalsUMAHandler::MediaInternalsUMAHandler() { |
| 308 registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_TERMINATED, | |
| 309 NotificationService::AllBrowserContextsAndSources()); | |
| 310 } | |
| 311 | |
| 312 void MediaInternals::MediaInternalsUMAHandler::Observe( | |
| 313 int type, | |
| 314 const NotificationSource& source, | |
| 315 const NotificationDetails& details) { | |
| 316 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
| 317 DCHECK_EQ(type, NOTIFICATION_RENDERER_PROCESS_TERMINATED); | |
| 318 RenderProcessHost* process = Source<RenderProcessHost>(source).ptr(); | |
| 319 | |
| 320 // Post the task to the IO thread to avoid race in updating renderer_info_ map | |
| 321 // by both SavePlayerState & LogAndClearPlayersInRenderer from different | |
| 322 // threads. | |
| 323 // Using base::Unretained() on MediaInternalsUMAHandler is safe since | |
| 324 // it is owned by MediaInternals and share the same lifetime | |
| 325 BrowserThread::PostTask( | |
| 326 BrowserThread::IO, FROM_HERE, | |
| 327 base::Bind(&MediaInternalsUMAHandler::LogAndClearPlayersInRenderer, | |
| 328 base::Unretained(this), process->GetID())); | |
| 329 } | 300 } |
| 330 | 301 |
| 331 void MediaInternals::MediaInternalsUMAHandler::SavePlayerState( | 302 void MediaInternals::MediaInternalsUMAHandler::SavePlayerState( |
| 332 const media::MediaLogEvent& event, | 303 int render_process_id, |
| 333 int render_process_id) { | 304 const media::MediaLogEvent& event) { |
| 334 DCHECK_CURRENTLY_ON(BrowserThread::IO); | 305 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 335 PlayerInfoMap& player_info = renderer_info_[render_process_id]; | 306 PlayerInfoMap& player_info = renderer_info_[render_process_id]; |
| 336 switch (event.type) { | 307 switch (event.type) { |
| 337 case media::MediaLogEvent::PIPELINE_ERROR: { | 308 case media::MediaLogEvent::PIPELINE_ERROR: { |
| 338 int status; | 309 int status; |
| 339 event.params.GetInteger("pipeline_error", &status); | 310 event.params.GetInteger("pipeline_error", &status); |
| 340 player_info[event.id].last_pipeline_status = | 311 player_info[event.id].last_pipeline_status = |
| 341 static_cast<media::PipelineStatus>(status); | 312 static_cast<media::PipelineStatus>(status); |
| 342 break; | 313 break; |
| 343 } | 314 } |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 432 media::PIPELINE_STATUS_MAX + 1); | 403 media::PIPELINE_STATUS_MAX + 1); |
| 433 } | 404 } |
| 434 // Report whether video decoder fallback happened, but only if a video decoder | 405 // Report whether video decoder fallback happened, but only if a video decoder |
| 435 // was reported. | 406 // was reported. |
| 436 if (!player_info.video_decoder.empty()) { | 407 if (!player_info.video_decoder.empty()) { |
| 437 UMA_HISTOGRAM_BOOLEAN("Media.VideoDecoderFallback", | 408 UMA_HISTOGRAM_BOOLEAN("Media.VideoDecoderFallback", |
| 438 player_info.video_decoder_changed); | 409 player_info.video_decoder_changed); |
| 439 } | 410 } |
| 440 } | 411 } |
| 441 | 412 |
| 442 void MediaInternals::MediaInternalsUMAHandler::LogAndClearPlayersInRenderer( | 413 void MediaInternals::MediaInternalsUMAHandler::OnProcessTerminated( |
| 443 int render_process_id) { | 414 int render_process_id) { |
| 444 DCHECK_CURRENTLY_ON(BrowserThread::IO); | 415 // Ensures to run on the IO thread to avoid race in |updating renderer_info_| |
| 416 // by both SavePlayerState and OnProcessTerminated. |
| 417 // Using base::Unretained() on MediaInternalsUMAHandler is safe since |
| 418 // it is owned by MediaInternals and shares the same lifetime. |
| 419 if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) { |
| 420 BrowserThread::PostTask( |
| 421 BrowserThread::IO, FROM_HERE, |
| 422 base::Bind(&MediaInternalsUMAHandler::OnProcessTerminated, |
| 423 base::Unretained(this), render_process_id)); |
| 424 return; |
| 425 } |
| 426 |
| 445 auto players_it = renderer_info_.find(render_process_id); | 427 auto players_it = renderer_info_.find(render_process_id); |
| 446 if (players_it == renderer_info_.end()) | 428 if (players_it == renderer_info_.end()) |
| 447 return; | 429 return; |
| 448 auto it = players_it->second.begin(); | 430 auto it = players_it->second.begin(); |
| 449 while (it != players_it->second.end()) { | 431 while (it != players_it->second.end()) { |
| 450 ReportUMAForPipelineStatus(it->second); | 432 ReportUMAForPipelineStatus(it->second); |
| 451 players_it->second.erase(it++); | 433 players_it->second.erase(it++); |
| 452 } | 434 } |
| 435 renderer_info_.erase(players_it); |
| 453 } | 436 } |
| 454 | 437 |
| 455 MediaInternals* MediaInternals::GetInstance() { | 438 MediaInternals* MediaInternals::GetInstance() { |
| 456 return g_media_internals.Pointer(); | 439 return g_media_internals.Pointer(); |
| 457 } | 440 } |
| 458 | 441 |
| 459 MediaInternals::MediaInternals() | 442 MediaInternals::MediaInternals() |
| 460 : can_update_(false), | 443 : can_update_(false), |
| 461 owner_ids_(), | 444 owner_ids_(), |
| 462 uma_handler_(new MediaInternalsUMAHandler()) { | 445 uma_handler_(new MediaInternalsUMAHandler()) { |
| 446 registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_TERMINATED, |
| 447 NotificationService::AllBrowserContextsAndSources()); |
| 463 } | 448 } |
| 464 | 449 |
| 465 MediaInternals::~MediaInternals() {} | 450 MediaInternals::~MediaInternals() {} |
| 466 | 451 |
| 452 void MediaInternals::Observe(int type, |
| 453 const NotificationSource& source, |
| 454 const NotificationDetails& details) { |
| 455 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 456 DCHECK_EQ(type, NOTIFICATION_RENDERER_PROCESS_TERMINATED); |
| 457 RenderProcessHost* process = Source<RenderProcessHost>(source).ptr(); |
| 458 |
| 459 uma_handler_->OnProcessTerminated(process->GetID()); |
| 460 |
| 461 base::AutoLock auto_lock(lock_); |
| 462 pending_events_map_.erase(process->GetID()); |
| 463 } |
| 464 |
| 465 // Converts the |event| to a |update|. Returns whether the conversion succeeded. |
| 466 static bool ConvertEventToUpdate(int render_process_id, |
| 467 const media::MediaLogEvent& event, |
| 468 base::string16* update) { |
| 469 DCHECK(update); |
| 470 |
| 471 base::DictionaryValue dict; |
| 472 dict.SetInteger("renderer", render_process_id); |
| 473 dict.SetInteger("player", event.id); |
| 474 dict.SetString("type", media::MediaLog::EventTypeToString(event.type)); |
| 475 |
| 476 // TODO(dalecurtis): This is technically not correct. TimeTicks "can't" be |
| 477 // converted to to a human readable time format. See base/time/time.h. |
| 478 const double ticks = event.time.ToInternalValue(); |
| 479 const double ticks_millis = ticks / base::Time::kMicrosecondsPerMillisecond; |
| 480 dict.SetDouble("ticksMillis", ticks_millis); |
| 481 |
| 482 // Convert PipelineStatus to human readable string |
| 483 if (event.type == media::MediaLogEvent::PIPELINE_ERROR) { |
| 484 int status; |
| 485 if (!event.params.GetInteger("pipeline_error", &status) || |
| 486 status < static_cast<int>(media::PIPELINE_OK) || |
| 487 status > static_cast<int>(media::PIPELINE_STATUS_MAX)) { |
| 488 return false; |
| 489 } |
| 490 media::PipelineStatus error = static_cast<media::PipelineStatus>(status); |
| 491 dict.SetString("params.pipeline_error", |
| 492 media::MediaLog::PipelineStatusToString(error)); |
| 493 } else { |
| 494 dict.Set("params", event.params.DeepCopy()); |
| 495 } |
| 496 |
| 497 *update = SerializeUpdate("media.onMediaEvent", &dict); |
| 498 return true; |
| 499 } |
| 500 |
| 467 void MediaInternals::OnMediaEvents( | 501 void MediaInternals::OnMediaEvents( |
| 468 int render_process_id, const std::vector<media::MediaLogEvent>& events) { | 502 int render_process_id, const std::vector<media::MediaLogEvent>& events) { |
| 469 DCHECK_CURRENTLY_ON(BrowserThread::IO); | 503 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 470 // Notify observers that |event| has occurred. | 504 // Notify observers that |event| has occurred. |
| 471 for (auto event = events.begin(); event != events.end(); ++event) { | 505 for (const auto& event : events) { |
| 472 base::DictionaryValue dict; | 506 if (CanUpdate()) { |
| 473 dict.SetInteger("renderer", render_process_id); | 507 base::string16 update; |
| 474 dict.SetInteger("player", event->id); | 508 if (ConvertEventToUpdate(render_process_id, event, &update)) |
| 475 dict.SetString("type", media::MediaLog::EventTypeToString(event->type)); | 509 SendUpdate(update); |
| 476 | |
| 477 // TODO(dalecurtis): This is technically not correct. TimeTicks "can't" be | |
| 478 // converted to to a human readable time format. See base/time/time.h. | |
| 479 const double ticks = event->time.ToInternalValue(); | |
| 480 const double ticks_millis = ticks / base::Time::kMicrosecondsPerMillisecond; | |
| 481 dict.SetDouble("ticksMillis", ticks_millis); | |
| 482 | |
| 483 // Convert PipelineStatus to human readable string | |
| 484 if (event->type == media::MediaLogEvent::PIPELINE_ERROR) { | |
| 485 int status; | |
| 486 if (!event->params.GetInteger("pipeline_error", &status) || | |
| 487 status < static_cast<int>(media::PIPELINE_OK) || | |
| 488 status > static_cast<int>(media::PIPELINE_STATUS_MAX)) { | |
| 489 continue; | |
| 490 } | |
| 491 media::PipelineStatus error = static_cast<media::PipelineStatus>(status); | |
| 492 dict.SetString("params.pipeline_error", | |
| 493 media::MediaLog::PipelineStatusToString(error)); | |
| 494 } else { | |
| 495 dict.Set("params", event->params.DeepCopy()); | |
| 496 } | 510 } |
| 497 | 511 |
| 498 if (CanUpdate()) | 512 SaveEvent(render_process_id, event); |
| 499 SendUpdate(SerializeUpdate("media.onMediaEvent", &dict)); | 513 uma_handler_->SavePlayerState(render_process_id, event); |
| 500 uma_handler_->SavePlayerState(*event, render_process_id); | |
| 501 } | 514 } |
| 502 } | 515 } |
| 503 | 516 |
| 504 void MediaInternals::AddUpdateCallback(const UpdateCallback& callback) { | 517 void MediaInternals::AddUpdateCallback(const UpdateCallback& callback) { |
| 505 DCHECK_CURRENTLY_ON(BrowserThread::UI); | 518 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 506 update_callbacks_.push_back(callback); | 519 update_callbacks_.push_back(callback); |
| 507 | 520 |
| 508 base::AutoLock auto_lock(lock_); | 521 base::AutoLock auto_lock(lock_); |
| 509 can_update_ = true; | 522 can_update_ = true; |
| 510 } | 523 } |
| 511 | 524 |
| 512 void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { | 525 void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { |
| 513 DCHECK_CURRENTLY_ON(BrowserThread::UI); | 526 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 514 for (size_t i = 0; i < update_callbacks_.size(); ++i) { | 527 for (size_t i = 0; i < update_callbacks_.size(); ++i) { |
| 515 if (update_callbacks_[i].Equals(callback)) { | 528 if (update_callbacks_[i].Equals(callback)) { |
| 516 update_callbacks_.erase(update_callbacks_.begin() + i); | 529 update_callbacks_.erase(update_callbacks_.begin() + i); |
| 517 break; | 530 break; |
| 518 } | 531 } |
| 519 } | 532 } |
| 520 | 533 |
| 521 base::AutoLock auto_lock(lock_); | 534 base::AutoLock auto_lock(lock_); |
| 522 can_update_ = !update_callbacks_.empty(); | 535 can_update_ = !update_callbacks_.empty(); |
| 523 } | 536 } |
| 524 | 537 |
| 525 bool MediaInternals::CanUpdate() { | 538 bool MediaInternals::CanUpdate() { |
| 526 base::AutoLock auto_lock(lock_); | 539 base::AutoLock auto_lock(lock_); |
| 527 return can_update_; | 540 return can_update_; |
| 528 } | 541 } |
| 529 | 542 |
| 543 void MediaInternals::SendHistoricalMediaEvents() { |
| 544 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 545 base::AutoLock auto_lock(lock_); |
| 546 for (const auto& pending_events : pending_events_map_) { |
| 547 for (const auto& event : pending_events.second) { |
| 548 base::string16 update; |
| 549 if (ConvertEventToUpdate(pending_events.first, event, &update)) |
| 550 SendUpdate(update); |
| 551 } |
| 552 } |
| 553 // Do not clear the map/list here so that refreshing the UI or opening a |
| 554 // second UI still works nicely! |
| 555 } |
| 556 |
| 530 void MediaInternals::SendAudioStreamData() { | 557 void MediaInternals::SendAudioStreamData() { |
| 531 base::string16 audio_stream_update; | 558 base::string16 audio_stream_update; |
| 532 { | 559 { |
| 533 base::AutoLock auto_lock(lock_); | 560 base::AutoLock auto_lock(lock_); |
| 534 audio_stream_update = SerializeUpdate( | 561 audio_stream_update = SerializeUpdate( |
| 535 "media.onReceiveAudioStreamData", &audio_streams_cached_data_); | 562 "media.onReceiveAudioStreamData", &audio_streams_cached_data_); |
| 536 } | 563 } |
| 537 SendUpdate(audio_stream_update); | 564 SendUpdate(audio_stream_update); |
| 538 } | 565 } |
| 539 | 566 |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 594 if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { | 621 if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| 595 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind( | 622 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind( |
| 596 &MediaInternals::SendUpdate, base::Unretained(this), update)); | 623 &MediaInternals::SendUpdate, base::Unretained(this), update)); |
| 597 return; | 624 return; |
| 598 } | 625 } |
| 599 | 626 |
| 600 for (size_t i = 0; i < update_callbacks_.size(); i++) | 627 for (size_t i = 0; i < update_callbacks_.size(); i++) |
| 601 update_callbacks_[i].Run(update); | 628 update_callbacks_[i].Run(update); |
| 602 } | 629 } |
| 603 | 630 |
| 631 void MediaInternals::SaveEvent(int process_id, |
| 632 const media::MediaLogEvent& event) { |
| 633 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 634 |
| 635 // Max number of saved updates allowed for one process. |
| 636 const size_t kMaxNumEvents = 128; |
| 637 |
| 638 // Do not save instantaneous events that happen frequently and have little |
| 639 // value in the future. |
| 640 if (event.type == media::MediaLogEvent::NETWORK_ACTIVITY_SET || |
| 641 event.type == media::MediaLogEvent::BUFFERED_EXTENTS_CHANGED) { |
| 642 return; |
| 643 } |
| 644 |
| 645 base::AutoLock auto_lock(lock_); |
| 646 auto& pending_events = pending_events_map_[process_id]; |
| 647 // TODO(xhwang): Notify user that some old logs could have been truncated. |
| 648 // See http://crbug.com/498520 |
| 649 if (pending_events.size() >= kMaxNumEvents) |
| 650 pending_events.pop_front(); |
| 651 pending_events.push_back(event); |
| 652 } |
| 653 |
| 604 void MediaInternals::SendAudioLogUpdate(AudioLogUpdateType type, | 654 void MediaInternals::SendAudioLogUpdate(AudioLogUpdateType type, |
| 605 const std::string& cache_key, | 655 const std::string& cache_key, |
| 606 const std::string& function, | 656 const std::string& function, |
| 607 const base::DictionaryValue* value) { | 657 const base::DictionaryValue* value) { |
| 608 if (!CanUpdate()) | 658 if (!CanUpdate()) |
| 609 return; | 659 return; |
| 610 | 660 |
| 611 { | 661 { |
| 612 base::AutoLock auto_lock(lock_); | 662 base::AutoLock auto_lock(lock_); |
| 613 const bool has_entry = audio_streams_cached_data_.HasKey(cache_key); | 663 const bool has_entry = audio_streams_cached_data_.HasKey(cache_key); |
| (...skipping 10 matching lines...) Expand all Loading... |
| 624 CHECK( | 674 CHECK( |
| 625 audio_streams_cached_data_.GetDictionary(cache_key, &existing_dict)); | 675 audio_streams_cached_data_.GetDictionary(cache_key, &existing_dict)); |
| 626 existing_dict->MergeDictionary(value); | 676 existing_dict->MergeDictionary(value); |
| 627 } | 677 } |
| 628 } | 678 } |
| 629 | 679 |
| 630 SendUpdate(SerializeUpdate(function, value)); | 680 SendUpdate(SerializeUpdate(function, value)); |
| 631 } | 681 } |
| 632 | 682 |
| 633 } // namespace content | 683 } // namespace content |
| OLD | NEW |